wxPython-2-8-应用开发秘籍-全-

wxPython 2.8 应用开发秘籍(全)

原文:zh.annas-archive.org/md5/bf3dbd6b96d5f04d0cead4622f834a62

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在当今的桌面应用程序世界中,能够开发能在多个环境中运行的应用程序具有极大的吸引力。目前,有几种选项可供跨平台框架在 Python 中开发桌面应用程序。wxPython 就是这样一种针对 Python 编程语言的跨平台 GUI 工具包。它允许 Python 程序员简单、轻松地创建具有完整、高度功能性的图形用户界面的程序。wxPython 的代码风格在多年中发生了很大变化,变得更加 Pythonic。本书中的示例都是最新的,反映了这种风格的变化。这本烹饪书为您提供了最新的食谱,以快速创建健壮、可靠和可重用的 wxPython 应用程序。这些食谱将指导您从编写简单的、基本的 wxPython 脚本开始,一直到复杂的概念,还介绍了 wxPython 中的各种设计方法和技巧。

本书从介绍各种主题开始,从 wxPython 应用程序的最基本需求,到框架内部运作的一些更深入细节,为任何 wxPython 应用程序打下基础。接着解释了事件处理、基本和高级用户界面控件、界面设计和布局、创建对话框、组件、扩展功能等内容。最后,我们学习如何构建和管理用于分发应用。

对于每个食谱,都有一个入门示例,然后是更高级的示例,以及大量的示例代码,展示了如何开发和维护用户友好的应用程序。对于经验更丰富的开发者,大多数食谱还包括对解决方案的额外讨论,让你能够进一步自定义和增强组件。

本书涵盖的内容

第一章, wxPython 入门,为您介绍了创建 wxPython 应用程序的基础知识。本章涵盖的主题将为您提供开始构建您自己的应用程序所需的信息,以及一些关于框架内部工作和结构的洞察。

第二章, 响应事件,展示了如何利用事件来驱动应用程序,并允许用户通过用户界面与之交互。本章首先概述了事件是什么以及它们是如何工作的,然后继续介绍如何与多种常见事件进行交互。

第三章, 用户界面基本构建块,讨论了众多对于创建几乎所有用户界面至关重要的基本小部件。在本章中,你将了解到按钮、菜单和工具栏等小部件的使用方法。

第四章,用户界面的高级构建块,向您介绍 wxPython 控件库中一些更高级的控件。这些控件将允许您创建标签式界面并在您的用户界面中显示更复杂类型的数据。

第五章, 提供信息和提醒用户,展示了多种保持应用程序用户了解正在发生的事情并提供他们在应用程序界面中与各种控件交互的帮助的技术。本章将向您展示如何使用各种工具提示控件、消息框和启动屏幕。

第六章, 从用户检索信息,介绍了使用常见对话框从用户那里获取信息以执行打开文件、搜索文本甚至打印等任务的方法。作为FileDialogFindDialogs使用方法的配方之一,你将创建一个类似记事本的应用程序。

第七章, 窗口布局与设计,在这里你将了解到许多用于在 wxPython 中设计用户界面的概念和技术。本章的大部分内容将解释如何使用 Sizers 来快速实现跨平台用户界面。

第八章,屏幕绘图,介绍了用户界面工作的基础知识,通过向您展示如何使用一些基本工具来实现您自己的自定义用户界面对象。本章将向您展示如何通过创建多个自定义显示控件来使用设备上下文执行自定义绘图例程。

第九章,设计方法和技巧,向您介绍了一些常见的编程模式,并解释了如何将它们应用到 wxPython 应用程序中。本章中的信息将为您提供一些强大的软件设计方法和技巧的理解,这些技巧不仅适用于编写 wxPython 应用程序,而且可以普遍应用于其他框架,以扩展您的编程工具箱。

第十章,创建组件和扩展功能,展示了如何扩展现有用户界面组件的功能,以及如何创建自己的控件。本章中的食谱将第二章、第七章、第八章和第九章中展示的许多信息结合起来,以创建新的控件并增强 wxPython 提供的一些基本控件的功能。

第十一章,使用线程和定时器创建响应式界面,深入探讨了并发编程的世界。本章向您展示了如何创建多线程应用程序,并涵盖了在与用户界面交互时,从工作线程中需要特别注意的事项,以便创建稳定且响应灵敏的界面。

第十二章, 构建和管理用于分发的应用程序,通过向您介绍一些增强任何将分发给最终用户的应用程序基础设施的有用配方,结束了对 wxPython 框架的巡礼。这包括如何存储配置信息、异常处理、国际化,以及如何创建和分发您应用程序的独立二进制文件。

你需要这本书的内容

要开始使用 wxPython,你只需要一个用于编辑 Python 源代码的文本编辑器。有很多选择可用,但在这里我将毫不掩饰地推荐我的应用程序 Editra,因为它包含在 wxPython 文档和演示包中,同时也可以在 editra.org 找到。它是用 wxPython 编写的,并为 Python 提供了良好的语法高亮和自动完成支持,这将有助于你学习 wxPython API。

这本书主要面向 Python 2.5/2.6 和 wxPython 2.8,尽管本书的内容也直接适用于 wxPython 的后续版本。以下为推荐的安装软件:

  1. Python 2.6 的最新版本(www.python.org/download/releases/2.6/).

  2. wxPython 2.8 的最新版本(www.wxpython.org/download.php).

这本书面向的对象

这本书是为想要开发图形用户界面应用的 Python 程序员所写。需要具备基本的 Python 知识和面向对象编程概念。

习惯用法

在这本书中,您将发现多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们含义的解释。

文本中的代码单词如下所示:"App对象还维护了MainLoop,该对象用于驱动 wxPython 应用程序"。

代码块设置如下:

import wx
class MyApp(wx.App):
def OnInit(self):
wx.MessageBox("Hello wxPython", "wxApp")
return True

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

class MyPanel(wx.Panel):
__metaclass__ = ClassSynchronizer
def __init__(self, parent, *args, **kwargs)

任何命令行输入或输出都应按照以下格式编写:

python setup.py py2exe

新术语重要词汇将以粗体显示。你在屏幕上看到的,例如在菜单或对话框中的文字,将以如下方式显示:“点击确定按钮关闭它并退出应用程序”。

注意事项

警告或重要提示会出现在这样的方框中。

注意

小技巧和窍门看起来是这样的。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或者可能不喜欢什么。读者的反馈对我们来说非常重要,以便我们开发出您真正能从中获得最大收益的书籍。

要向我们发送一般性反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。

如果您有一本书需要我们出版,并希望看到它,请通过www.packtpub.com上的建议书名表单发送给我们,或者发送电子邮件至<suggest@packtpub.com>

如果你在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,链接为www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者了,我们有许多事情可以帮助你从你的购买中获得最大收益。

小贴士

下载本书的示例代码

您可以从www.PacktPub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support并注册,以便直接将文件通过电子邮件发送给您。

错误清单

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/support,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的任何现有勘误列表中。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有勘误。

海盗行为

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接发送至 <copyright@packtpub.com> 与我们联系,以提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。

问题

如果你在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章. 开始使用 wxPython

在本章中,我们将介绍几乎所有 wxPython 应用程序的基础组件,例如:

  • 应用对象

  • 主框架

  • 理解窗口层次结构

  • 引用控件

  • 使用位图

  • 向窗口添加图标

  • 利用库存编号

  • 访问剪贴板

  • 支持拖放

  • 两阶段小部件创建

  • 理解继承限制

简介

在当今的桌面应用程序世界中,能够开发能在多个操作系统和桌面平台上运行的应用程序具有极大的吸引力。目前,有少数几个跨平台的 Python 框架可以用来开发桌面应用程序。wxPython 库是一组针对 wxWidgets 库的 Python 绑定,wxWidgets 是一个功能强大的跨平台 C++应用程序框架,可用于创建用户界面。wxPython 与众不同的地方在于,与其他绘制自己控制器的 UI 工具包不同,wxPython 使用平台自身的原生 UI 工具包来创建和显示 UI 组件。这意味着 wxPython 应用程序将具有与系统上其他应用程序相同的视觉和感觉,因为它使用的是与系统其他部分相同的控件和主题。

在 wxPython 中开发应用程序为编写可在 Windows、Macintosh OS X、Linux 和其他类似 UNIX 环境中运行的应用程序提供了极大的灵活性。应用程序可以在一个平台上快速开发,并且通常只需进行很少或不需要任何修改就可以部署到另一个平台。

应用对象

App 对象启动库并初始化底层工具包。所有 wxPython 应用程序都必须创建一个 App 对象。这应该在尝试创建任何其他 GUI 对象之前完成,以确保库的所有依赖部分都已正确初始化。App 对象还维护 MainLoop,它用于驱动 wxPython 应用程序。

本食谱将演示所有 wxPython 应用程序都可以构建的基本模式。

如何做到这一点...

在这里,我们将创建一个类似于 "Hello World" 的应用程序,以展示 wxPython 应用程序的基本结构:

import wx

class MyApp(wx.App):
    def OnInit(self):
        wx.MessageBox("Hello wxPython", "wxApp") 
        return True

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

运行上一个脚本将在屏幕上显示以下弹出对话框。点击确定以关闭它并退出应用程序。

如何做...

它是如何工作的...

应用对象在创建时会调用其OnInit方法。此方法被重写并用作初始化此应用程序的主要入口点。通过返回True,该方法通知框架可以继续进行。OnInit是大多数应用程序进行初始化和创建主窗口(s)的地方。

在这个例子中,我们通过将False作为第一个参数传递来创建App对象。这个参数用于告诉 wxPython 是否要重定向输出。在开发应用程序时,建议始终将此设置为False,并从命令行运行脚本,这样你就可以看到在通过双击脚本运行时可能被遗漏的任何错误输出。

在创建应用程序对象并且所有初始化完成之后,你需要做的最后一件事就是调用App对象的MainLoop方法来启动事件循环。此方法将不会返回,直到最后一个顶级窗口被销毁或者直到App对象被告知退出。wxPython 是一个事件驱动系统,而MainLoop是这个系统的核心。在循环的每次迭代中,事件被分发以执行 GUI 中的所有任务,例如处理鼠标点击、移动窗口和重绘屏幕。

还有更多...

wx.App 类构造函数有四个可选的关键字参数:

wWx.App((redirect=True, filename=None, 
       useBestVisual=False,clearSigInt=True))

四个可选的关键字参数如下:

  • redirect: 重定向 stdout

  • filename: 如果重定向为 True,则可以用来指定要重定向到的输出文件。

  • useBestVisual: 指定应用程序是否应尝试使用底层工具包提供的最佳视觉效果。(它对大多数系统没有影响。)

  • clearSigInt: 是否应该清除SIGINT?将此设置为True将允许通过按下Ctrl + C来终止应用程序,就像大多数其他应用程序一样。

主框架

对于大多数应用,您可能希望显示一个窗口供用户与之交互。在 wxPython 中,最典型的窗口对象被称为Frame。本食谱将向您展示如何派生一个Frame并在应用程序中显示它。

如何做到这一点...

此示例在先前的配方基础上扩展,添加了一个最小的空应用程序窗口:

import wx

class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, title="The Main Frame")
        self.SetTopWindow(self.frame)
        self.frame.Show()

        return True

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title,
                                      pos, size, style, name)

        # Attributes
        self.panel = wx.Panel(self)

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

运行前面的代码将会显示如下窗口:

如何做...

它是如何工作的...

Frame 是大多数应用程序的主要顶级窗口和容器。让我们首先检查我们的 MyFrame 类。在这个类中,有一件重要的事情需要注意。我们创建了一个 Panel 对象,作为 Frame 的子窗口。你可以把 Panel 看作是包含其他控件的一个盒子。此外,为了使 Frame 在所有平台上都能正确运行和显示,它必须有一个 Panel 作为其主要子窗口。

首先,在我们的 AppOnInit 方法中,我们创建了一个 MyFrame 实例,并将 None 作为其第一个参数传递。这个参数用于指定 Frame 的父窗口。因为这是我们主窗口,所以我们传递 None 来表示它没有父窗口。其次,我们调用我们的 AppSetTopWindow 方法,以便将新创建的 MyFrame 实例设置为应用程序的顶级窗口。最后,我们调用 FrameShow 方法;这仅仅做了它名字所暗示的事情,即显示 Frame,以便用户可以看到它,尽管 Frame 不会在屏幕上实际可见,直到 MainLoop 开始运行。

还有更多...

Frame 类在其构造函数中具有多个样式标志,可以设置这些标志以修改窗口的行为和外观。这些样式标志可以组合成一个位掩码,并作为构造函数的样式参数的值提供。下表概述了一些常见的样式标志。所有可用样式的完整列表可以在 wxPython 在线文档中找到,网址为 wxpython.org/onlinedocs.php

样式标志 描述

| wx.DEFAULT_FRAME_STYLE | 这是以下标志的按位或运算:

  • wx.MINIMIZE_BOX

  • wx.MAXIMIZE_BOX

  • wx.RESIZE_BORDER

  • wx.SYSTEM_MENU

  • wx.CAPTION

  • wx.CLOSE_BOX

  • wx.CLIP_CHILDREN

|

wx.MINIMIZE_BOX 显示一个最小化窗口的标题栏按钮
wx.MAXIMIZE_BOX 显示一个最大化窗口的标题栏按钮
wx.CLOSE_BOX 显示一个标题栏按钮,允许关闭框架。(即“X”按钮)
wx.RESIZE_BORDER 允许用户通过拖动边框来调整框架的大小
wx.CAPTION 显示在框架上的标题
wx.SYSTEM_MENU 显示系统菜单(即在 Windows 上单击框架图标时显示的菜单)
wx.CLIP_CHILDREN 消除因背景重绘引起的闪烁(仅限 Windows)

理解窗口层次结构

wxPython 中的所有不同窗口和控制都有包含的层次结构。一些控制可以作为其他控制的容器,而另一些则不能。本食谱旨在帮助理解这一层次结构。

准备就绪

我们将对之前食谱中的Frame进行微小的修改,所以让我们打开那个食谱的代码,为新的更改做好准备。

如何做到这一点...

这里是即将替换我们现有 Frame 类的新的代码。

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title,
                                      pos, size, style, name)

        # Attributes
        self.panel = wx.Panel(self)
        self.panel.SetBackgroundColour(wx.BLACK)
        self.button = wx.Button(self.panel,
                                label="Push Me",
                                pos=(50, 50))

它是如何工作的...

基本上,存在三种按以下包含顺序分层的窗口对象类别:

  • 顶级窗口(框架和对话框)

  • 通用容器(面板和笔记本,...)

  • 控件(按钮、复选框、组合框等)

顶级窗口位于层次结构的顶部,它可以包含任何类型的窗口,除了另一个顶级窗口。接下来是通用容器,它们可以任意地包含任何其他通用容器或控件。最后,在层次结构的底部是控件。这些是用户将与之交互的 UI 的功能部分。在某些情况下,它们可以用来包含其他控件,但通常不会这样做。包含层次结构与控件的父母层次结构相连接。父控件将是其子控件的容器。

当运行前面的示例时,这个层次结构变得明显。正如我们之前看到的,Frame 是最外层的容器对象;接下来你可以看到 Panel,我们将它变成了黑色以便更明显;最后你可以看到 Button,它是作为 Panel 的子对象被添加的。

参见

  • 本章中关于引用控制的配方提供了进一步解释,说明了窗口层次结构是如何相互连接的。

引用控制

应用程序中的所有 Window 对象以各种方式相互连接。通常情况下,获取一个控件实例的引用非常有用,这样你就可以对控件执行某些操作或从中检索一些数据。本食谱将展示一些用于查找和获取控件引用的可用功能。

如何做到这一点...

在这里,我们将之前菜谱中的MyFrame类扩展,使其在按钮被点击时具有事件处理器。在事件处理器中,我们可以看到一些在运行时访问我们 UI 中不同控件的方法:

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title,
                                      pos, size, style, name)

        # Attributes
        self.panel = wx.Panel(self)
        self.panel.SetBackgroundColour(wx.BLACK)
        button = wx.Button(self.panel,
                           label="Get Children",
                           pos=(50, 50))
        self.btnId = button.GetId()

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnButton, button)

    def OnButton(self, event):
        """Called when the Button is clicked"""
        print "\nFrame GetChildren:"
        for child in self.GetChildren():
            print "%s" % repr(child)

        print "\nPanel FindWindowById:"
        button = self.panel.FindWindowById(self.btnId)
        print "%s" % repr(button)
        # Change the Button's label
        button.SetLabel("Changed Label")

        print "\nButton GetParent:"
        panel = button.GetParent()
        print "%s" % repr(panel)

        print "\nGet the Application Object:"
        app = wx.GetApp()
        print "%s" % repr(app)

        print "\nGet the Frame from the App:"
        frame = app.GetTopWindow()
        print "%s" % repr(frame)

它是如何工作的...

框架中的每个窗口都保存对其父窗口和子窗口的引用。现在运行我们的程序将打印出使用所有窗口都有的访问器函数来查找和检索对其子窗口和其他相关控件引用的结果。

  • GetChildren: 此方法将返回给定控件的所有子控件的列表

  • FindWindowById: 这可以通过使用其 ID 来查找特定的子窗口

  • GetParent: 此方法将检索窗口的父窗口

  • wx.GetApp: 这是一个全局函数,用于获取唯一的应用对象访问权限

  • App.GetTopWindow: 这将获取应用程序中的主顶层窗口

点击按钮将会调用OnButton方法。在OnButton中,有一些示例展示了如何使用上述每种方法。每个方法都会返回一个 GUI 对象的引用。在我们的例子中,对Panel调用GetChildren将返回其子控件列表。遍历这个列表,我们将打印出每个子控件,在这个情况下,就是按钮。FindWindowById可以用来查找特定的子控件;同样,我们也在我们的Panel上调用这个方法来查找按钮控件。为了展示我们已经找到了按钮,我们使用了它的SetLabel方法来更改其标签。接下来,对按钮调用GetParent将返回按钮的父对象,即Panel。最后,通过使用全局的GetApp函数,我们可以获取到应用程序对象的引用。App对象的GetTopWindow将返回对我们 Frame 的引用。

还有更多...

这里有一些更多有用的方法来获取控件引用。

函数名称 描述
wx.FindWindowByLabel(label) 通过标签查找子窗口
wx.FindWindowByName(name) 通过名称查找子窗口
wx.GetTopLevelParent() 获取顶层窗口,该窗口位于给定控件父级层次结构的顶部

参见

  • 本章中关于理解窗口层次结构的配方概述了窗口如何在其中包含以及它们之间如何相互关联的结构。

使用位图

很可能,在某个时刻,你将希望能够在你的应用程序中显示一张图片。Bitmap 是用于在应用程序中显示图片的基本数据类型。本食谱将展示如何将图片文件加载到 Bitmap 中,并在 Frame 中显示它。

如何做到这一点...

要了解如何使用位图,我们将创建一个小应用程序,从硬盘加载一张图片并在一个框架中显示它:

import os
import wx

class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, title="Bitmaps")
        self.SetTopWindow(self.frame)
        self.frame.Show()

        return True

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,  
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title,
                                      pos, size, style, name)

        # Attributes
        self.panel = wx.Panel(self)

        img_path = os.path.abspath("./face-grin.png")
        bitmap = wx.Bitmap(img_path, type=wx.BITMAP_TYPE_PNG)
        self.bitmap = wx.StaticBitmap(self.panel, 
                                      bitmap=bitmap)

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

它是如何工作的...

StaticBitmap 控件是在应用程序中显示位图的简单方法。在随本食谱附带的示例代码中,我们有一个与我们的脚本在同一目录下的图像,名为 face-grin.png,我们希望显示该图像。为了显示图像,我们首先使用 Bitmap 构造函数将图像加载到内存中,然后将其传递给 StaticBitmap 控件以在屏幕上显示图像。构造函数接受文件路径和一个指定图像格式的类型参数。

还有更多...

内置了对最常见图像格式的支持。以下列表显示了支持的图像文件格式:

  • wx.BITMAP_TYPE_ANY

  • wx.BITMAP_TYPE_BMP

  • wx.BITMAP_TYPE_ICO

  • wx.BITMAP_TYPE_CUR

  • wx.BITMAP_TYPE_XBM

  • wx.BITMAP_TYPE_XPM

  • wx.BITMAP_TYPE_TIF

  • wx.BITMAP_TYPE_GIF

  • wx.BITMAP_TYPE_PNG

  • wx.BITMAP_TYPE_JPEG

  • wx.BITMAP_TYPE_PNM

  • wx.BITMAP_TYPE_PCX

  • wx.BITMAP_TYPE_PICT

  • wx.BITMAP_TYPE_ICON

  • wx.BITMAP_TYPE_ANI

  • wx.BITMAP_TYPE_IFF

参见

  • 第三章中的使用工具栏配方,用户界面基本构建块包含了一些更多的位图使用示例。

  • 在第十章的自定义 ArtProvider 配方中,创建组件和扩展功能提供了更多关于如何创建位图的详细信息。

添加图标到 Windows

将图标添加到应用程序的标题栏中,作为品牌化应用程序的一种方式,这有助于将其与其他在桌面上运行的应用程序区分开来。本食谱将展示如何轻松地将图标添加到框架中。

注意

在 OS X 系统上,wxPython 2.8 目前不支持在标题栏添加图标。

如何做到这一点...

在这里,我们将创建一个Frame子类,该子类从硬盘加载一个图像文件并在其标题栏上显示它:

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title, pos,
                                      size, style, name)

        # Attributes
        self.panel = wx.Panel(self)

        # Setup
        path = os.path.abspath("./face-monkey.png")
        icon = wx.Icon(path, wx.BITMAP_TYPE_PNG)
        self.SetIcon(icon)

显示这个 Frame 子类将导致出现如下窗口。与主 Frame 配方中的窗口相比,您可以看到标题左侧的新图标:

如何做...

它是如何工作的...

在这个示例中,我们有一个小(16x16)的猴子图像,我们希望将其显示在Frame的标题栏中。为了简单起见,此图像位于我们的脚本相同的目录中,我们使用相对路径来加载它。Frame需要一个图标而不是Bitmap,因此我们必须使用Icon将我们的图像加载到内存中。在加载图像后,剩下的只是调用FrameSetIcon方法来设置Frame的图标。

参见

  • 本章中的使用位图配方讨论了更常用的位图图像类型。

利用库存编号

所有控件以及许多其他用户界面元素,例如菜单,在其构造函数中接受一个 ID 作为参数,该参数可用于在事件处理程序中识别控件或对象。通常,使用wx.ID_ANY的值让系统自动为项目生成一个 ID,或者使用wx.NewId函数创建一个新的 ID。然而,wx模块中也有许多预定义的 ID,这些 ID 对于许多应用程序中常见的某些项目具有特殊含义,例如复制/粘贴菜单项或确定/取消按钮。这些项目的一些预期行为和外观可能会因平台而异。通过使用标准 ID,wxPython 将为您处理这些差异。本食谱将展示这些 ID 可以在哪些地方派上用场。

如何做到这一点...

这段代码片段展示了如何利用一些预定义的 ID 来简化创建一些常见 UI 元素的过程:

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title, 
                                      pos, size, style, name)

        # Attributes
        self.panel = wx.Panel(self)

        # Setup
        ok_btn = wx.Button(self.panel, wx.ID_OK)
        cancel_btn = wx.Button(self.panel, wx.ID_CANCEL,
                               pos=(100, 0))

        menu_bar = wx.MenuBar()
        edit_menu = wx.Menu()
        edit_menu.Append(wx.NewId(), "Test")
        edit_menu.Append(wx.ID_PREFERENCES)
        menu_bar.Append(edit_menu, "Edit")
        self.SetMenuBar(menu_bar)

上一节课将创建以下窗口:

如何做...

它是如何工作的...

在这个菜谱中首先要注意的是,我们创建的两个按钮没有指定标签。通过使用“确定”和“取消”的库存 ID 作为它们的 ID,框架将自动为控件添加正确的标签。

这同样适用于菜单项,如在我们编辑菜单中查看的“偏好设置”项所示。另一个需要注意的重要事项是,如果在这个示例中运行在 Macintosh OS X 上,框架也会自动将“偏好设置”菜单项移动到应用程序菜单中预期的位置。

还有更多...

在模态对话框中使用具有库存 ID 的按钮也将允许对话框被关闭,并返回适当的值,例如wx.OKwx.CANCEL,无需将事件处理程序连接到按钮以执行此标准操作。通过使用StdDialogButtonSizer与库存 ID,也可以自动获取对话框的正确按钮布局。

参见

  • 第三章中的 创建股票按钮 菜单,用户界面基本构建块 展示了如何使用股票 ID 来构建标准按钮。

  • 第七章中的标准对话框按钮布局配方,窗口布局和设计展示了如何通过使用库存 ID 轻松地将常用按钮添加到对话框中。

  • 第十二章中的针对 OS X 优化配方展示了 Stock IDs 的更多用途。

访问剪贴板

剪贴板是一种跨应用、可访问的方式,用于在不同应用程序之间传输数据。本食谱将展示如何从剪贴板获取文本,以及如何将文本放入剪贴板以便其他应用程序访问。

如何做到这一点...

以下两个函数可以用来从剪贴板获取文本和将文本放置到剪贴板:

def SetClipboardText(text):
    """Put text in the clipboard
    @param text: string
    """
    data_o = wx.TextDataObject()
    data_o.SetText(text)
    if wx.TheClipboard.IsOpened() or wx.TheClipboard.Open():
        wx.TheClipboard.SetData(data_o)
        wx.TheClipboard.Close()

def GetClipboardText():
    """Get text from the clipboard
    @return: string
    """
    text_obj = wx.TextDataObject()
    rtext = ""
    if wx.TheClipboard.IsOpened() or wx.TheClipboard.Open():
        if wx.TheClipboard.GetData(text_obj):
            rtext = text_obj.GetText()
        wx.TheClipboard.Close()
    return rtext

它是如何工作的...

wxPython 提供了一个单例剪贴板对象,可以用来与系统剪贴板进行交互。这个类与用于表示底层系统数据类型的对象一起工作。使用剪贴板是一个三步过程:

  • 打开剪贴板

  • 设置/获取数据对象

  • 关闭剪贴板

还有更多...

剪贴板支持许多其他数据类型,而不仅仅是文本。wxPython 提供了对一些附加类型的内置支持,以及用于定义您自己的自定义类型的类。这些不同数据类型的用法遵循与 TextDataObject 相同的一般模式。

数据类型 描述
wx.BitmapDataObject 用于从剪贴板获取位图并将其放置在剪贴板上
wx.CustomDataObject 可以存储任何可 Python 可序列化的数据类型
wx.DataObjectComposite 可以包含任意数量的简单数据类型,并使它们一次性全部可用
wx.FileDataObject 用于存储文件名
wx.URLDataObject 用于存储 URL

参见

  • 本章中关于支持拖放的配方与剪贴板相关,因为它允许在应用程序之间传输数据。

支持拖放

为了提高可用性,在应用程序中支持拖放操作是很好的,这样用户就可以简单地拖放文件或其他对象到您的应用程序中。本食谱将展示如何支持接受一个同时支持文件和文本的CompositeDataObject

如何做到这一点...

首先,我们将定义一个自定义的拖放目标类:

class FileAndTextDropTarget(wx.PyDropTarget):
    """Drop target capable of accepting dropped 
    files and text
    """
    def __init__(self, file_callback, text_callback):
        assert callable(file_callback)
        assert callable(text_callback)
        super(FileAndTextDropTarget, self).__init__()wx.PyDropTarget.__init__(self)

        # Attributes
        self.fcallback = file_callback # Drop File Callback
        self.tcallback = text_callback # Drop Text Callback
        self._data = None
        self.txtdo = None
        self.filedo = None

        # Setup
        self.InitObjects()

    def InitObjects(self):
        """Initializes the text and file data objects"""
        self._data = wx.DataObjectComposite()
        self.txtdo = wx.TextDataObject()
        self.filedo = wx.FileDataObject()
        self._data.Add(self.txtdo, False)
        self._data.Add(self.filedo, True)
        self.SetDataObject(self._data)

    def OnData(self, x_cord, y_cord, drag_result):
        """Called by the framework when data is dropped 
        on the target
        """
        if self.GetData():
            data_format = self._data.GetReceivedFormat()
            if data_format.GetType() == wx.DF_FILENAME:
                self.fcallback(self.filedo.GetFilenames())
            else:
                self.tcallback(self.txtdo.GetText())

        return drag_result

然后为了使用FileAndTextDropTarget,我们使用窗口对象的SetDropTarget方法将其分配给一个窗口。

class DropTargetFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="DropTargetFrame"):
        super(DropTargetFrame, self).__init__(parent, id,
                                              title, pos,
                                              size, style,
                                              name)

        # Attributes
        choices = ["Drag and Drop Text or Files here",]
        self.list = wx.ListBox(self, 
                               choices=choices)
        self.dt = FileAndTextDropTarget(self.OnFileDrop,
                                        self.OnTextDrop)
        self.list.SetDropTarget(self.dt)

        # Setup
        self.CreateStatusBar()

    def OnFileDrop(self, files):
        self.PushStatusText("Files Dropped")
        for f in files:
            self.list.Append(f)

    def OnTextDrop(self, text):
        self.PushStatusText("Text Dropped")
        self.list.Append(text)

它是如何工作的...

当窗口接收到拖放数据时,框架将调用我们的DropTargetOnData方法。当OnData被调用时,我们只需从我们的DataObject中获取数据,并将其传递给适当的回调函数,以便我们的窗口决定如何处理这些数据。

所有窗口对象都有一个SetDropTarget方法,可以用来分配一个DropTarget,因此这个类可以用于几乎任何类型的控件。在先前的示例中,我们将它分配给了一个ListBox,然后在我们每个回调函数中将拖放的数据添加到列表中。

还有更多...

PyDropTarget 类提供了一些可以在拖动操作的不同时间点调用的方法。这些方法也可以被重写,以便执行诸如更改鼠标光标、显示自定义拖动图像或拒绝拖动对象等操作。

方法 当方法被调用时
OnEnter(x, y, drag_result) 当拖动对象进入窗口时被调用。返回一个拖动结果值(即,wx.DragNone, wx.DragCopy, ...)
OnDragOver(x, y, drag_result) 在鼠标拖动对象到目标上时调用
OnLeave() 当鼠标离开拖放目标时调用
OnDrop(x, y) 当用户放下对象时被调用。返回 True 以接受对象或返回 False 以拒绝它
OnData(x, y, drag_result) OnDrop 被调用后,当数据对象被接受时调用

参见

  • 本章中的访问剪贴板配方展示了另一种在应用程序之间进行数据传输的方法。

两阶段小部件创建

两阶段小部件创建是一种通过两步初始化小部件及其 UI 部分的方法。这种对象创建方法被类工厂如 XRC(XML 资源)所使用,以及用于设置不能通过构造函数的常规样式参数设置的其他样式标志。本食谱将展示如何使用两阶段创建来创建一个具有特殊按钮的框架,该按钮可用于将其置于上下文相关帮助模式。

注意事项

这是一个特定于 Windows 的示例;其他平台不支持在它们的标题栏中拥有ContextButton

如何做到这一点...

在这里,我们将创建一个Frame子类,它使用两阶段创建来设置一个额外的样式标志:

class MyFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        pre = wx.PreFrame()
        pre.SetExtraStyle(wx.FRAME_EX_CONTEXTHELP)
        pre.Create(parent, *args, **kwargs)
        self.PostCreate(pre)

它是如何工作的...

在 wxPython 中,两阶段小部件创建实际上是一个三步过程。首先,每个支持它的类都有自己的PreClass,它用作一个工厂构造函数,预先创建对象。在这个阶段,预对象可以用来设置额外的样式标志。下一步是调用CreateCreate的行为类似于常规构造函数,创建控制器的 UI 部分。最后一步是调用PostCreatePostCreatepre对象转换为self,使得对象看起来就像类的__init__方法已经被正常调用一样。

参见

  • 第七章中的使用 XRC配方,窗口布局与设计讨论了 XRC。

理解继承限制

wxPython 是围绕 wxWidgets C++ 框架的包装器。这种关系意味着在大多数 wxPython 对象内部存在一个 C++ 对象。因此,属于 wxPython 类的方法并不能总是像在普通 Python 对象中那样被覆盖。

为了展示这种行为,这个菜谱将展示如何创建一个类,该类会自动将其子窗口添加到其Sizer布局中。这将与一个不向 Python 层公开其虚方法的类进行对比。

如何做到这一点...

为了展示重写方法之间的差异,我们首先将创建两个相似的类,从一个继承自标准Panel类的类开始:

import wx

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super(MyPanel, self).__init__(parent)

        sizer = wx.BoxSizer()
        self.SetSizer(sizer)

    def AddChild(self, child):
        sizer = self.GetSizer()
        sizer.Add(child, 0, wx.ALIGN_LEFT|wx.ALL, 8)
        return super(MyPanel, self).AddChild(child)

现在我们将创建一个与Py版本完全相同的类,只是它派生自该类:

class MyVirtualPanel(wx.PyPanel):
    """Class that automatically adds children
    controls to sizer layout.
    """
    def __init__(self, parent):
        super(MyVirtualPanel, self).__init__(parent)

        sizer = wx.BoxSizer()
        self.SetSizer(sizer)

    def AddChild(self, child):
        sizer = self.GetSizer()
        sizer.Add(child, 0, wx.ALIGN_LEFT|wx.ALL, 8)
        return super(MyVirtualPanel, self).AddChild(child)

现在下面我们有一个使用上述两个类的小型示例应用:

class MyFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(MyFrame, self).__init__(parent,
                                      *args, **kwargs)

        # Attributes
        self.mypanel = MyPanel(self)
        self.mypanel.SetBackgroundColour(wx.BLACK)
        self.virtpanel = MyVirtualPanel(self)
        self.virtpanel.SetBackgroundColour(wx.WHITE)

        # Setup
        self.__DoLayout()

    def __DoLayout(self):
        """Layout the window"""
        # Layout the controls using a sizer
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.mypanel, 1, wx.EXPAND)
        sizer.Add(self.virtpanel, 1, wx.EXPAND)
        self.SetSizer(sizer)

        # Create 3 children for the top panel
        for x in range(3):
            wx.Button(self.mypanel,
                      label="MyPanel %d" % x)
        # Create 3 children for the bottom panel
        for x in range(3):
            wx.Button(self.virtpanel,
                      label="VirtPanel %d" % x)

        self.SetInitialSize(size=(300, 200))

class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None,
                             title="Virtualized Methods")
        self.SetTopWindow(self.frame)
        self.frame.Show()

        return True

if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

运行此代码将显示如下窗口:

如何做...

它是如何工作的...

在我们Panel类的每个版本中,我们都重写了AddChild方法,该方法在每次创建新的子窗口时被调用。当这种情况发生时,AddChild方法在类的 C++部分中被调用,因此为了能够在我们的 Python 类版本中重写该方法,我们需要使用提供访问从 C++类中虚拟化方法重写的特殊版本。

在 wxPython 中,那些带有以Py为前缀的类版本,暴露了许多方法的虚拟化版本,因此当它们在 Python 子类中被覆盖时,它们会被绑定到对象 C++层的相应方法,并且将由框架调用而不是基类的实现。

这可以在上面展示的我们的食谱应用截图中看到。那个不继承自PyPanel的类的顶部版本,其三个按钮都堆叠在窗口的左上角,因为它的重写AddChild方法从未被调用。另一方面,那个继承自PyPanel的类的版本调用了其AddChild方法,并且能够在其Sizer中布局按钮

还有更多...

关于哪些方法是作为虚方法暴露的,哪些不是,并没有很好地记录。这里有一个小技巧可以帮助你识别给定类中可用的虚方法。只需在 Python 解释器中运行以下代码:

import wx
for method in dir(wx.PyPanel):
    if method.startswith('base_'):
        print method

dir() 调用中的参数可以更改为您想要检查的任何类。运行此命令将打印出该类中所有虚拟化的方法列表。base_ 方法是由 SWIG 在 wxPython 绑定到 wxWidgets 的过程中生成的,不应直接在您的代码中使用。相反,应使用没有 base_ 前缀的方法。

参见

  • 在第十章 创建自定义控件 的“创建组件和扩展功能”中,创建自定义控件的配方展示了更多重写虚拟方法的用法示例。

  • 第七章中的使用 BoxSizer配方,窗口设计和布局,解释了如何使用 BoxSizer 类在窗口中执行控件布局。

第二章:响应事件

在本章中,我们将涵盖:

  • 处理事件

  • 理解事件传播

  • 处理键事件

  • 使用 UpdateUI 事件

  • 操纵鼠标

  • 创建自定义事件类

  • 使用 EventStack 管理事件处理器

  • 使用验证器验证输入

  • 处理 Apple 事件

简介

在事件驱动系统中,事件被用来将框架内的动作与那些事件相关联的回调函数连接起来。基于事件驱动框架构建的应用程序利用这些事件来确定何时响应由用户或系统发起的动作。在用户界面中,事件是了解何时点击了按钮、选择了菜单或用户在与应用程序界面交互时可能采取的广泛多样的其他动作的方式。

正如你所见,了解如何应对应用生命周期中发生的事件是创建一个功能应用的关键部分。因此,让我们深入 wxPython 的事件驱动世界吧。

处理事件

wxPython 是一个事件驱动系统。该系统的使用方法在框架中非常直接且规范。无论你的应用程序将交互的控制或事件类型如何,处理事件的基模式都是相同的。本食谱将介绍在 wxPython 事件系统中的基本操作方法。

如何做到这一点...

让我们创建一个简单的Frame,其中包含两个按钮,以展示如何处理事件:

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title,
                                      pos, size, style, name)

        # Attributes
        self.panel = wx.Panel(self)

        self.btn1 = wx.Button(self.panel, label="Push Me")
        self.btn2 = wx.Button(self.panel, label="push me too")

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.btn1, 0, wx.ALL, 10)
        sizer.Add(self.btn2, 0, wx.ALL, 10)
        self.panel.SetSizer(sizer)

        self.Bind(wx.EVT_BUTTON, self.OnButton, self.btn1)
        self.Bind(wx.EVT_BUTTON,
                  lambda event:
                  self.btn1.Enable(not self.btn1.Enabled),
                  self.btn2)

    def OnButton(self, event):
        """Called when self.btn1 is clicked"""
        event_id = event.GetId()
        event_obj = event.GetEventObject()
        print "Button 1 Clicked:"
        print "ID=%d" % event_id
        print "object=%s" % event_obj.GetLabel()

它是如何工作的...

在这个菜谱中需要注意的代码行是两个 Bind 调用。Bind 方法用于将事件处理函数与可能发送到控件的事件关联起来。事件总是沿着窗口层次结构向上传播,而不会向下传播。在这个例子中,我们将按钮事件绑定到了 Frame,但事件将起源于 Panel 的子 Button 对象。Frame 对象位于包含 Panel 的层次结构的顶部,而 Panel 又包含两个 Buttons。正因为如此,由于事件回调既没有被 Button 也没有被 Panel 处理,它将传播到 Frame,在那里我们的 OnButton 处理程序将被调用。

Bind 方法需要两个必填参数:

  • 事件绑定对象 (EVT_FOO)

  • 一个接受事件对象作为其第一个参数的可调用对象。这是当事件发生时将被调用的事件处理函数。

可选参数用于指定绑定事件处理器的源控件。在这个示例中,我们通过将Button对象指定为Bind函数的第三个参数,为每个按钮绑定了一个处理器。

EVT_BUTTON 是当应用程序用户点击 Button 时的事件绑定器。当第一个按钮被点击时,事件处理器 OnButton 将被调用以通知我们的程序这一动作已发生。事件对象将作为其第一个参数传递给处理器函数。事件对象有多个方法可以用来获取有关事件及其来源控件的信息。每个事件可能都有不同的数据可用,这取决于与事件来源的控件类型相关的事件类型。

对于我们的第二个按钮,我们使用了lambda函数作为创建事件处理函数的简写方式,无需定义新的函数。这是一种处理只需执行简单操作的事件的便捷方法。

参见

  • 第一章中的 应用程序对象 菜单,使用 wxPython 入门 讲述了主循环,这是事件系统的核心。

  • 第一章中的理解窗口层次结构配方描述了窗口包含层次结构。

  • 第三章中的 创建股票按钮 菜单,用户界面基本构建块 详细解释了按钮。

  • 第七章中的使用 BoxSizer 布局配方,窗口布局与设计解释了如何使用BoxSizer类来布局控件。

理解事件传播

在 wxPython 中,主要有两种事件对象,每种都有其独特的行为:

  • 事件

  • 命令事件

基本事件(Events)是指不会在窗口层次结构中向上传播的事件。相反,它们保持在它们被发送到的或起源的窗口的本地。第二种类型,CommandEvents,是更常见的事件类型,它们与常规事件的不同之处在于,它们会沿着窗口父级层次结构向上传播,直到被处理或到达应用程序对象的末尾。本食谱将探讨如何处理、理解和控制事件的传播。

如何做到这一点...

为了探索事件如何传播,让我们创建另一个简单的应用程序:

import wx

ID_BUTTON1 = wx.NewId()
ID_BUTTON2 = wx.NewId()

class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, title="Event Propagation")
        self.SetTopWindow(self.frame)
        self.frame.Show()

        self.Bind(wx.EVT_BUTTON, self.OnButtonApp)

        return True

    def OnButtonApp(self, event):
        event_id = event.GetId()
        if event_id == ID_BUTTON1:
            print "BUTTON ONE Event reached the App Object"

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE,
                 name="MyFrame"):
        super(MyFrame, self).__init__(parent, id, title,
                                      pos, size, style, name)

        # Attributes
        self.panel = MyPanel(self)

        self.btn1 = wx.Button(self.panel, ID_BUTTON1,
                              "Propagates")
        self.btn2 = wx.Button(self.panel, ID_BUTTON2,
                              "Doesn't Propagate")

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.btn1, 0, wx.ALL, 10)
        sizer.Add(self.btn2, 0, wx.ALL, 10)
        self.panel.SetSizer(sizer)

        self.Bind(wx.EVT_BUTTON, self.OnButtonFrame)

    def OnButtonFrame(self, event):
        event_id = event.GetId()
        if event_id == ID_BUTTON1:
            print "BUTTON ONE event reached the Frame"
            event.Skip()
        elif event_id == ID_BUTTON2:
            print "BUTTON TWO event reached the Frame"

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super(MyPanel, self).__init__(parent)

        self.Bind(wx.EVT_BUTTON, self.OnPanelButton)

    def OnPanelButton(self, event):
        event_id = event.GetId()
        if event_id == ID_BUTTON1:
            print "BUTTON ONE event reached the Panel"
            event.Skip()
        elif event_id == ID_BUTTON2:
            print "BUTTON TWO event reached the Panel"
            # Not skipping the event will cause its 
            # propagation to end here
if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

运行此程序将创建一个带有两个按钮的应用程序。点击每个按钮以查看事件如何不同地传播。

它是如何工作的...

事件处理程序链的调用将从事件起源的对象开始。在这种情况下,它将是我们的两个按钮之一。此应用程序窗口层次结构的每个级别都绑定了一个通用事件处理程序,它将接收任何按钮事件。

点击第一个按钮将显示所有的事件处理程序都被调用。这是因为对于第一个按钮,我们调用了事件的Skip方法。在事件上调用Skip将告诉它继续传播到事件处理程序层次结构中的下一级。这将是显而易见的,因为控制台将打印出三条语句。另一方面,点击第二个按钮将只调用一个事件处理程序,因为未调用Skip

参见

  • 本章中的处理事件菜谱解释了事件处理器是如何工作的。

  • 第一章中的理解窗口层次结构配方,使用 wxPython 入门描述了事件传播通过的窗口层次结构。

处理关键事件

KeyEvents是与键盘操作相关的事件。许多控件可以接受键盘事件。每次在键盘上按下键时,都会向具有键盘焦点的控件发送两个或三个事件,具体取决于按下了哪个键。本食谱将创建一个简单的文本编辑窗口,以演示如何使用KeyEvents来过滤添加到TextCtrl中的文本。

如何做到这一点...

要查看一些KeyEvents的实际应用,让我们创建一个简单的窗口,该窗口上有一个TextCtrl

class MyFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(MyFrame, self).__init__(parent, *args, **kwargs)

        # Attributes
        self.panel = wx.Panel(self)
        self.txtctrl = wx.TextCtrl(self.panel, 
                                   style=wx.TE_MULTILINE)

        # Layout
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.txtctrl, 1, wx.EXPAND)
        self.panel.SetSizer(sizer)
        self.CreateStatusBar() # For output display

        # Event Handlers
        self.txtctrl.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.txtctrl.Bind(wx.EVT_CHAR, self.OnChar)
        self.txtctrl.Bind(wx.EVT_KEY_UP, self.OnKeyUp)

    def OnKeyDown(self, event):
        """KeyDown event is sent first"""
        print "OnKeyDown Called"
        # Get information about the event and log it to
        # the StatusBar for display.
        key_code = event.GetKeyCode()
        raw_code = event.GetRawKeyCode()
        modifiers = event.GetModifiers()
        msg = "key:%d,raw:%d,modifers:%d" % \
              (key_code, raw_code, modifiers)
        self.PushStatusText("KeyDown: " + msg)

        # Must Skip the event to allow OnChar to be called
        event.Skip()

    def OnChar(self, event):
        """The Char event comes second and is
        where the character associated with the
        key is put into the control.
        """
        print "OnChar Called"
        modifiers = event.GetModifiers()
        key_code = event.GetKeyCode()
        # Beep at the user if the Shift key is down
        # and disallow input.
        if modifiers & wx.MOD_SHIFT:
            wx.Bell()
        elif chr(key_code) in "aeiou":elif unichr(key_code) in   "aeiou":
            # When a vowel is pressed append a
            # question mark to the end.
            self.txtctrl.AppendText("?")
        else:
            # Let the text go in to the buffer
            event.Skip()

    def OnKeyUp(self, event):
        """KeyUp comes last"""
        print "OnKeyUp Called"
        event.Skip()

当在这个窗口中输入时,按下Shift键将不允许输入文本,并且会将所有元音字母转换成问号。

它是如何工作的...

KeyEvents 是按照以下顺序由系统发送的:

  • EVT_KEY_DOWN

  • EVT_CHAR(仅适用于与字符相关联的键)

  • EVT_KEY_UP

重要的是要注意,我们在 TextCtrl 上调用了 Bind 而不是 Frame。这是必要的,因为 KeyEvents 只会发送到具有键盘焦点的控件,在这个窗口中将是 TextCtrl

每个 KeyEvent 都附带有一些属性,用于指定在事件中按下了哪个键以及在此事件期间按下了哪些其他修饰键,例如 Shift, AltCtrl 键。

在事件上调用 Skip 允许控制处理它,并调用链中的下一个处理器。例如,不在 EVT_KEY_DOWN 处理器中跳过事件将阻止 EVT_CHAREVT_KEY_UP 处理器被调用。

在这个示例中,当键盘上的一个键被按下时,我们的OnKeyDown处理程序首先被调用。我们在那里的所有操作只是将一条消息printstdout,并在StatusBar中显示一些关于事件的详细信息,然后调用Skip。然后,在我们的OnChar处理程序中,我们通过检查事件修改器掩码中是否包含Shift 键来对大写字母进行一些简单的过滤。如果是,我们向用户发出蜂鸣声,并且不调用事件上的Skip,以防止字符出现在TextCtrl中。此外,作为一个修改事件行为的示例,我们将原始键码转换为字符字符串,并检查该键是否为元音。如果是元音键,我们只需在TextCtrl中插入一个问号。最后,如果事件在OnChar处理程序中被跳过,我们的OnKeyUp处理程序将被调用,在那里我们简单地打印一条消息到stdout以显示它已被调用。

还有更多...

一些控件需要在它们的构造函数中指定wx.WANTS_CHARS样式标志,以便接收字符事件。Panel类是要求使用这种特殊样式标志以接收EVT_CHAR事件的常见示例。通常,这用于在创建一个从Panel派生的自定义控件类型时执行特殊处理。

参见

  • 本章中关于使用验证器验证输入的配方使用KeyEvents来执行输入验证。

使用 UpdateUI 事件

UpdateUIEvents 是框架定期发送的事件,以便允许应用程序更新其控件的状态。这些事件对于执行诸如根据应用程序的业务逻辑更改控件启用或禁用时间等任务非常有用。本食谱将展示如何使用 UpdateUIEvents 根据 UI 的当前上下文更新菜单项的状态。

如何做到这一点...

在本例中,我们创建了一个简单的窗口,其中包含一个编辑菜单和一个文本控件编辑菜单中有三个项目,这些项目将根据文本控件当前的选中状态通过使用UpdateUIEvents来启用或禁用。

class TextFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(TextFrame, self).__init__(parent,
                                        *args,
                                        **kwargs)

        # Attributes
        self.panel = wx.Panel(self)
        self.txtctrl = wx.TextCtrl(self.panel,
                                   value="Hello World",
                                   style=wx.TE_MULTILINE)

        # Layout
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.txtctrl, 1, wx.EXPAND)
        self.panel.SetSizer(sizer)
        self.CreateStatusBar() # For output display

        # Menu
        menub = wx.MenuBar()
        editm = wx.Menu()
        editm.Append(wx.ID_COPY, "Copy\tCtrl+C")
        editm.Append(wx.ID_CUT, "Cut\tCtrl+X")
        editm.Append(ID_CHECK_ITEM, "Selection Made?",
                     kind=wx.ITEM_CHECK)
        menub.Append(editm, "Edit")
        self.SetMenuBar(menub)

        # Event Handlers
        self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateEditMenu)

    def OnUpdateEditMenu(self, event):
        event_id = event.GetId()
        sel = self.txtctrl.GetSelection()
        has_sel = sel[0] != sel[1]
        if event_id in (wx.ID_COPY, wx.ID_CUT):
            event.Enable(has_sel)
        elif event_id == ID_CHECK_ITEM:
            event.Check(has_sel)
        else:
            event.Skip()

它是如何工作的...

UpdateUIEvents 是在空闲时间由框架定期发送的,以便应用程序检查控件的状态是否需要更新。我们的 TextFrame 类在其编辑菜单中有三个菜单项,这些菜单项将由我们的 OnUpdateUI 事件处理器管理。在 OnUpdateUI 中,我们检查事件的 ID 以确定事件是为哪个对象发送的,然后调用事件上的适当 UpdateUIEvent 方法来更改控件的状态。我们每个菜单项的状态取决于 TextCtrl 中是否有选择。调用 TextCtrlGetSelection 方法将返回一个包含选择开始和结束位置的元组。当两个位置不同时,控件中有选择,我们将 Enable CopyCut 项目,或者在我们的 Selection Made 项目中设置勾选标记。如果没有选择,则项目将变为禁用或未勾选。

在事件对象上调用该方法以更新控件,而不是在控件本身上调用该方法,这是非常重要的,因为它将允许更高效地更新。请参阅 wxPython API 文档中的UpdateUIEvent,以查看可用的所有方法列表。

还有更多...

UpdateUIEvent 类中提供了一些静态方法,这些方法允许应用程序更改事件传递的行为。其中最显著的两个方法如下:

  1. wx.UpdateUIEvent.SetUpdateInterval

  2. wx.UpdateUIEvent.SetMode

SetUpdateInterval 可以用来配置 UpdateUIEvents 发送频率。它接受一个表示毫秒数的参数。如果你发现在你的应用程序中处理 UpdateUIEvents 存在明显的开销,这将非常有用。你可以使用这个方法来降低这些事件发送的速率。

SetMode 可以用来配置哪些窗口将接收事件的行为,通过设置以下模式之一:

模式 描述
wx.UPDATE_UI_PROCESS_ALL 处理所有窗口的 UpdateUI 事件
wx.UPDATE_UI_PROCESS_SPECIFIED 仅处理设置了 WS_EX_PROCESS_UI_UPDATES 额外样式标志的窗口的 UpdateUI 事件。

参见

  • 本章中的 使用 EventStack 管理事件处理器 菜谱展示了如何集中管理 UpdateUI 事件。

操纵鼠标

MouseEvents 可以用来与用户在窗口内进行的鼠标位置变化和鼠标按钮点击进行交互。本教程将快速介绍一些在程序中可用的常见鼠标事件。

如何做到这一点...

在这里,我们以一个示例来创建一个简单的Frame类,其中包含一个Panel和一个按钮,以了解如何与MouseEvents进行交互。

class MouseFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(MouseFrame, self).__init__(parent,
                                         *args,
                                         **kwargs)

        # Attributes
        self.panel = wx.Panel(self)
        self.btn = wx.Button(self.panel)

        # Event Handlers
        self.panel.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
        self.panel.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
        self.panel.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.panel.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)

    def OnEnter(self, event):
        """Called when the mouse enters the panel"""
        self.btn.SetForegroundColour(wx.BLACK)
        self.btn.SetLabel("EVT_ENTER_WINDOW")
        self.btn.SetInitialSize()

    def OnLeave(self, event):
        """Called when the mouse leaves the panel"""
        self.btn.SetLabel("EVT_LEAVE_WINDOW")
        self.btn.SetForegroundColour(wx.RED)

    def OnLeftDown(self, event):
        """Called for left down clicks on the Panel"""
        self.btn.SetLabel("EVT_LEFT_DOWN")

    def OnLeftUp(self, event):
        """Called for left clicks on the Panel"""
        position = event.GetPosition()
        self.btn.SetLabel("EVT_LEFT_UP")
        # Move the button
        self.btn.SetPosition(position - (25, 25))

它是如何工作的...

在这个菜谱中,我们利用了鼠标光标进入Panel和左键点击Panel的事件来修改我们的Button。当鼠标光标进入窗口区域时,会向其发送一个EVT_ENTER_WINDOW事件;相反,当光标离开窗口时,它会收到一个EVT_LEAVE_WINDOW事件。当鼠标进入或离开Panel的区域时,我们更新按钮的标签以显示发生了什么。当我们的Panel接收到左键点击事件时,我们将Button移动到点击发生的位置。

重要的是要注意,我们直接在 Panel 上调用了 Bind 而不是在 Frame 上。这很重要,因为 MouseEvents 不是 CommandEvents,所以它们只会被发送到它们起源的窗口,而不会在包含层次结构中传播。

还有更多...

有大量可以用来与其他鼠标操作交互的 MouseEvents。以下表格包含了对每个事件的快速参考:

鼠标事件 描述
wx.EVT_MOUSEWHEEL 发送鼠标滚轮滚动事件。请参阅属于 MouseEvent 类的 GetWheelRotationGetWheelDelta 方法,了解如何处理此事件。
wx.EVT_LEFT_DCLICK 发送用于左鼠标按钮双击的事件。
wx.EVT_RIGHT_DOWN 当鼠标右键被按下时发送。
wx.EVT_RIGHT_UP 当鼠标右键被释放时发送。
wx.EVT_RIGHT_DCLICK 发送用于右键鼠标双击的事件。
wx.EVT_MIDDLE_DOWN 当鼠标中键被按下时发送。
wx.EVT_MIDDLE_UP 当鼠标中键被释放时发送。
wx.EVT_MIDDLE_DCLICK 发送于鼠标中键双击事件。
wx.EVT_MOTION 每当鼠标光标在窗口内移动时发送。
wx.EVT_MOUSE_EVENTS 此事件绑定器可用于获取所有鼠标相关事件的通告。

参见

  • 本章中关于理解事件传播的配方讨论了不同类型的事件是如何传播的。

创建自定义事件类

有时有必要定义自己的事件类型来表示自定义操作,以及从应用的一个地方传输数据到另一个地方。本食谱将展示创建自定义事件类的两种方法。

如何做到这一点...

在这个简短片段中,我们使用两种不同的方法定义了两种新的事件类型:

import wx
import wx.lib.newevent

# Our first custom event
MyEvent, EVT_MY_EVENT = wx.lib.newevent.NewCommandEvent()

# Our second custom event
myEVT_TIME_EVENT = wx.NewEventType()
EVT_MY_TIME_EVENT = wx.PyEventBinder(myEVT_TIME_EVENT, 1)
class MyTimeEvent(wx.PyCommandEvent):
    def __init__(self, id=0, time="12:00:00"):
         evttype = myEVT_TIME_EVENT
        super(MyTimeEvent, self).__init__(evttype, id)wx.PyCommandEvent.__init__(self, myEVT_TIME_EVENT, id)

        # Attributes
        self.time = time

    def GetTime(self):
        return self.time

它是如何工作的...

第一个示例展示了创建自定义事件类的最简单方法。来自 wx.lib.newevent 模块的 NewCommandEvent 函数将返回一个包含新事件类及其事件绑定器的元组。返回的类定义可以用来构建事件对象。当你只需要一个新的事件类型而不需要随事件发送任何自定义数据时,这种方法创建新事件类型最为有用。

为了使用事件对象,该对象需要通过事件循环进行处理。这里有两种方法可以实现,其中之一是PostEvent函数。PostEvent函数接受两个参数:第一个是需要接收事件的窗口,第二个是事件本身。例如,以下两行代码可以用来创建并发送我们的自定义MyEvent实例到Frame:

event = MyEvent(eventID)
wx.PostEvent(myFrame, event)

发送事件进行处理的第二种方式是使用窗口的ProcessEvent方法:

event = MyEvent(eventID)
myFrame.GetEventHandler().ProcessEvent(event)

这两者的区别在于,PostEvent会将事件放入应用程序的事件队列中,以便在MainLoop的下一个迭代中处理,而ProcessEvent则会导致事件立即被处理。

第二种方法展示了如何从PyCommandEvent基类派生出一个新的事件类型。为了以这种方式创建一个事件,需要完成以下三个步骤。

  1. 使用 NewEventType 函数定义一个新的事件类型。

  2. 使用PyEventBinder类创建事件绑定对象,该对象将事件类型作为其第一个参数。

  3. 定义用于创建事件对象的的事件类。

这个MyTimeEvent类可以保存一个自定义值,我们使用它来发送格式化的时间字符串。必须从PyCommandEvent派生这个类,这样我们附加到这个对象上的自定义 Python 数据和方法才能通过事件系统传递。

这些事件现在可以通过使用PostEvent函数或 Windows 的ProcessEvent方法发送到任何事件处理对象。这两种方法中的任何一种都会导致事件被分发到通过调用Bind与之关联的事件处理程序。

参见

  • 第一章中的理解继承限制配方,wxPython 入门解释了为什么需要某些类的 Py 版本。

  • 本章的处理事件配方讨论了事件处理器的使用。

使用 EventStack 管理事件处理器

EventStackwx.lib 模块中的一个模块,它提供了一个用于 wx 应用对象混合类,可用于帮助管理 MenuUpdateUI 事件的处理器。在具有多个顶级窗口或需要根据具有焦点的控件切换调用处理器的上下文的应用程序中,这可能很有用。本食谱将展示一个用于管理基于 Frame 的应用程序中事件的简单框架,这些应用程序使用了 AppEventHandlerMixin 类。本食谱附带的一个完整的工作示例展示了如何使用本食谱中的类。

如何做到这一点...

使用此代码,我们定义了两个相互协作的类。首先,我们定义了一个使用AppEventHandlerMixinApp基类。

import wx
import wx.lib.eventStack as eventStack 

class EventMgrApp(wx.App, eventStack.AppEventHandlerMixin):
    """Application object base class that
    event handler managment.
    """
    def __init__(self, *args, **kwargs):
        eventStack.AppEventHandlerMixin.__init__(self)
        wx.App.__init__(self, *args, **kwargs)

class EventMgrFrame(wx.Frame):
    """Frame base class that provides event
    handler managment.
    """
    def __init__(self, parent, *args, **kwargs):
        super(EventMgrFrame, self).__init__(parent,
                                            *args,
                                            **kwargs)

        # Attributes
        self._menu_handlers = []
        self._ui_handlers = []

        # Event Handlers
        self.Bind(wx.EVT_ACTIVATE, self._OnActivate)

    def _OnActivate(self, event):
        """Pushes/Pops event handlers"""
        app = wx.GetApp()
        active = event.GetActive()
        if active:
            mode = wx.UPDATE_UI_PROCESS_SPECIFIED
            wx.UpdateUIEvent.SetMode(mode)
            self.SetExtraStyle(wx.WS_EX_PROCESS_UI_UPDATES)

            # Push this instances handlers
            for handler in self._menu_handlers:
                app.AddHandlerForID(*handler)

            for handler in self._ui_handlers:
                app.AddUIHandlerForID(*handler)
        else:
            self.SetExtraStyle(0)
            wx.UpdateUIEvent.SetMode(wx.UPDATE_UI_PROCESS_ALL)
            # Pop this instances handlers
            for handler in self._menu_handlers:
                app.RemoveHandlerForID(handler[0])

            for handler in self._ui_handlers:
                app.RemoveUIHandlerForID(handler[0])

    def RegisterMenuHandler(self, event_id, handler):
        """Register a MenuEventHandler
        @param event_id: MenuItem ID
        @param handler: Event handler function
        """
        self._menu_handlers.append((event_id, handler))

    def RegisterUpdateUIHandler(self, event_id, handler):
        """Register a controls UpdateUI handler
        @param event_id: Control ID
        @param handler: Event handler function
        """
        self._ui_handlers.append((event_id, handler))

它是如何工作的...

EventMgrApp 类只是一个用于创建使用 AppEventHandlerMixin 的应用程序对象的基类。这个 mixin 提供了添加和删除 MenuEventUpdateUIEvent 处理程序的事件处理程序的方法。

EventMgrFrame 类是用于派生的框架的基类。此类将处理使用其 RegisterMenuHandlerRegisterUpdateUIHandler 方法注册的事件处理器的添加、移除和绑定。这些方法负责将事件处理器添加到堆栈中,当 Frame 被激活或停用时,这些处理器将被推入或弹出。AppEventHandlerMixin 将内部管理这些处理器的绑定和解绑。

参见

  • 本章中关于使用 UpdateUI 事件的配方详细讨论了UpdateUI事件。

使用验证器验证输入

验证器 是一种用于验证数据和过滤输入到控制器的事件的通用辅助类。大多数接受用户输入的控制都可以动态地与一个 Validator 关联。本食谱将展示如何创建一个 Validator,该 Validator 检查输入到窗口中的数据是否是一个在给定值范围内的整数。

如何做到这一点...

在这里,我们将定义一个用于TextCtrl的验证器,它可以用来验证输入的值是否为整数,并且是否在给定的范围内。

import wx
import sys

class IntRangeValidator(wx.PyValidator):
    """An integer range validator for a TextCtrl"""
    def __init__(self, min_=0, max_=sys.maxint):
        """Initialize the validator
        @keyword min: min value to accept
        @keyword max: max value to accept

        """
        super(IntRangeValidator, self).__init__()
        assert min_ >= 0, "Minimum Value must be >= 0"
        self._min = min_
        self._max = max_

        # Event managment
        self.Bind(wx.EVT_CHAR, self.OnChar)

    def Clone(self):
        """Required override"""
        return IntRangeValidator(self._min, self._max)

    def Validate(self, win):
        """Override called to validate the window's value.
        @return: bool
        """
        txtCtrl = self.GetWindow()
        val = txtCtrl.GetValue()
        isValid = False
        if val.isdigit():
            digit = int(val)
            if digit >= self._min and digit <= self._max:
                isValid = True

        if not isValid:
            # Notify the user of the invalid value
            msg = "Value must be between %d and %d" % \
                  (self._min, self._max)
            wx.MessageBox(msg,
                          "Invalid Value",
                          style=wx.OK|wx.ICON_ERROR)

        return isValid

    def OnChar(self, event):
        txtCtrl = self.GetWindow()
        key = event.GetKeyCode()
        isDigit = False
        if key < 256:
            isDigit = chr(key).isdigit()

        if key in (wx.WXK_RETURN,
                   wx.WXK_DELETE,
                   wx.WXK_BACK) or \
           key > 255 or isDigit:
            if isDigit:
                # Check if in range
                val = txtCtrl.GetValue()
                digit = chr(key)
                pos = txtCtrl.GetInsertionPoint()
                if pos == len(val):
                    val += digit
                else:
                    val = val[:pos] + digit + val[pos:]

                val = int(val)
                if val < self._min or val > self._max:
                    if not wx.Validator_IsSilent():
                        wx.Bell()
                    return

            event.Skip()
            return

        if not wx.Validator_IsSilent():
            # Beep to warn about invalid input
            wx.Bell()

        return

    def TransferToWindow(self):
         """Overridden to skip data transfer"""
         return True

    def TransferFromWindow(self):
         """Overridden to skip data transfer"""
         return True

它是如何工作的...

Validator 类包含多个需要重写以使其正常工作的虚拟方法。因此,为了访问类的虚拟方法感知版本,重要的是从 PyValidator 类而不是从 Validator 类派生一个子类。

所有 Validator 子类都必须重写 Clone 方法。这个方法只需简单地返回 Validator 的一个副本。

调用 Validate 方法来检查值是否有效。如果控件是模态对话框的子控件,在调用 EndModal 方法为“确定”按钮之前,将会调用此方法。这是通知用户任何输入问题的好时机。

校验器也可以绑定到它们窗口可能绑定的任何事件,并且可以用来过滤事件。在事件被发送到窗口之前,这些事件将被发送到校验器OnChar方法,允许校验器过滤哪些事件被允许到达控件。

如果您希望在Dialog显示或关闭时仅进行验证,则可以重写TransferToWindowTransferFromWindow方法。当Dialog显示时,将调用TransferToWindow,而当Dialog关闭时,将调用TransferFromWIndow。从任一方法返回True表示数据有效,而返回False则表示存在无效数据。

参见

  • 第一章中的理解继承限制配方讨论了类和重写虚拟方法使用 Python 版本的情况。

  • 本章中关于处理关键事件的配方详细讨论了KeyEvents

处理苹果事件

AppleEvents 是 Macintosh 操作系统使用的高级系统事件,用于在进程之间传递信息。为了处理诸如打开拖放到应用程序图标上的文件等操作,应用程序必须处理这些事件。wxPython 应用程序对象通过在应用程序对象中实现虚拟覆盖,提供了一些对最常见事件的内置支持。本食谱将展示如何创建一个可以利用内置的并且相对隐藏的事件回调函数的应用程序对象。

注意事项

这是一个针对 OS X 的特定配方,对其他平台将没有任何影响。

如何做到这一点...

这个小示例应用程序展示了在App中可用的所有内置回调方法,用于处理一些常见的AppleEvents

import wx

class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, title="AppleEvents")
        self.SetTopWindow(self.frame)
        self.frame.Show()

        return True

    def MacNewFile(self):
        """Called for an open-application event"""
        self.frame.PushStatusText("MacNewFile Called")

    def MacOpenFile(self, filename):
        """Called for an open-document event"""
        self.frame.PushStatusText("MacOpenFile: %s" % \
                                  filename)

    def MacOpenURL(self, url):
        """Called for a get-url event"""
        self.frame.PushStatusText("MacOpenURL: %s" % url)

    def MacPrintFile(self, filename):
        """Called for a print-document event"""
        self.frame.PushStatusText("MacPrintFile: %s" % \
                                   filename)

    def MacReopenApp(self):
        """Called for a reopen-application event"""
        self.frame.PushStatusText("MacReopenApp")
        # Raise the application from the Dock
        if self.frame.IsIconized():
            self.frame.Iconize(False)
        self.frame.Raise()

它是如何工作的...

对于一些常见的 AppleEvents,有五种内置的处理方法。要在您的应用程序中使用它们,只需在应用程序对象中覆盖它们,如前所述。由于应用程序对这些事件的响应非常特定于应用程序本身,这个方法除了在调用方法时向框架的状态栏报告之外,并没有做太多的事情。

应该实现的最常见的两个事件是 MacOpenFileMacReopenApp 方法,因为这些是实现在 OS X 上应用程序中标准预期行为所必需的。当用户将文件拖放到应用程序的 Dock 图标上时,会调用 MacOpenFile。在这种情况下,它将作为参数传递文件的路径。当用户左键单击正在运行的应用程序的 Dock 图标时,会调用 MacReopenApp。如配方中所示,这用于将应用程序带到前台,或者从 Dock 中的最小化状态中恢复出来。

还有更多...

在 wxPython 应用程序中添加对更多 AppleEvents 的支持是可能的,尽管这不是一项特别容易的任务,因为它需要编写一个本地扩展模块来捕获事件,阻塞 wx 的 EventLoop,然后在处理事件后将 Python 解释器的状态恢复到 wx。wxPython Wiki 中有一个相当不错的示例可以作为起点(见 wiki.wxpython.org/Catching%20AppleEvents%20in%20wxMAC),如果你发现自己需要走这条路的话。

参见

  • 第一章中的理解继承限制配方,使用 wxPython 入门包含了更多关于重写虚方法的信息。

  • 第十二章中的针对 OS X 优化配方,在应用程序基础设施部分包含了更多关于使 wxPython 应用程序在 OS X 上运行良好的信息。

第三章:用户界面的基本构建块

在本章中,我们将涵盖:

  • 创建股票按钮

  • 按钮,按钮,还有更多的按钮

  • 提供带有复选框的选项

  • 使用 TextCtrl

  • 使用 Choice 控件提供选择

  • 添加 菜单菜单栏

  • ToolBars 一起工作

  • 如何使用 PopupMenus

  • 使用StaticBox分组控件

简介

即使是最复杂的对象通常也是由许多较小的、更简单的对象或部分组成的。应用开发者的任务是利用这些较小的部分并将它们以有意义的方式连接起来,以达到应用所需的功能。为了能够构建应用,有必要知道有哪些部分可供使用。

wxPython 提供了大量类和实用工具。实际上,基本集合非常丰富,以至于完全有可能在不发明任何自定义部分的情况下构建一个功能齐全的应用程序。所以让我们开始,看看几乎在任何桌面应用程序中都能找到的一些最常见和基本的部分。

创建股票按钮

几乎所有应用程序都包含按钮,在这些按钮中有很多常见的按钮,例如“确定”和“取消”,它们反复出现。在 wxPython 中,这些常见的按钮被称为库存按钮(Stock Buttons),因为它们是通过传递一个库存 ID 给Button构造函数来构建的。

如何做到这一点...

让我们创建一个简单的面板,在其上放置四个按钮,以查看如何创建股票按钮:

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super(MyPanel, self).__init__(parent)

        # Make some buttons
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        for bid in (wx.ID_OK, wx.ID_CANCEL,
                    wx.ID_APPLY, wx.ID_HELP):
            button = wx.Button(self, bid)
            sizer.Add(button, 0, wx.ALL, 5)
        self.SetSizer(sizer)

它是如何工作的...

常见按钮是通过使用带有库存 ID 但没有标签的标准Button创建的。框架随后将为当前平台创建正确类型的按钮并带有适当的标签。每个平台对于这些常见按钮都有略微不同的标准。通过使用库存按钮,这些跨平台差异可以由框架处理。例如,查看以下两个屏幕截图,展示了之前示例代码在 Windows 7 和 OS X 上分别运行的情况。

Windows 7 的屏幕截图:

如何工作...

OS X 的屏幕截图:

如何工作...

注意

平台通知:在 Linux 上,根据 GTK 的版本,标准按钮也将显示适当的主题图标。

还有更多...

可以从几乎所有的股票 ID 创建股票按钮。如果你的文本编辑器不提供自动完成提示,这里有一个快速查看所有可用股票 ID 的方法:只需在你的 Python 解释器中运行以下代码,以检查wx命名空间中的所有 ID 常量。

import wx
for x in dir(wx):
    if x.startswith(‘ID_’):
        print x

参见

  • 第一章中的使用股票 ID配方,使用 wxPython 入门详细讨论了用于构建股票按钮的 ID。

  • 本章中的 按钮、按钮,还有更多按钮 菜单展示了如何使用 wxPython 中可用的其他按钮类。

  • 第七章“窗口布局与设计”中的标准对话框按钮布局配方展示了如何使用 Stock Buttons 在对话框中实现易于控制的布局。

按钮,按钮,还有更多的按钮

普通的 Button 类仅允许在按钮上显示标签。如果这对你应用的某些需求来说略显单调,那么你很幸运。wxPython 还提供了多种不同外观和感觉的按钮类型,以及扩展的功能。本食谱将介绍 wxPython 中可用的其他一些按钮控件。

注意

版本说明:以下代码中使用的agw包和GradientButton类仅在 wxPython 2.8.9.2 及以后的版本中可用。

如何做到这一点...

要查看这些不同的按钮的外观以及它们能做什么,我们将创建一个简单的面板,其中包含这些额外按钮类的不同示例:

import wx
import wx.lib.platebtn as platebtn
import wx.lib.agw.gradientbutton as gradbtn

class ButtonTestPanel(wx.Panel):
    def __init__(self, parent):
        super(ButtonTestPanel, self).__init__(parent)

        # Attributes
        # Make a ToggleButton
        self.toggle = wx.ToggleButton(self,
                                      label="Toggle Button")

        # Make a BitmapButton
        bmp = wx.Bitmap("./face-monkey.png",
                        wx.BITMAP_TYPE_PNG)
        self.bmpbtn = wx.BitmapButton(self, bitmap=bmp)

        # Make a few PlateButton variants
        self.pbtn1 = pbtn.PlateButton(self,
                                      label="PlateButton")
        self.pbtn2 = pbtn.PlateButton(self, 
                                      label="PlateBmp",
                                      bmp=bmp)
        style = pbtn.PB_STYLE_SQUARE
        self.pbtn3 = pbtn.PlateButton(self,
                                      label="Square Plate",
                                      bmp=bmp,
                                      style=style)
        self.pbtn4 = pbtn.PlateButton(self,
                                      label="PlateMenu")
        menu = wx.Menu()
        menu.Append(wx.NewId(), text="Hello World")
        self.pbtn4.SetMenu(menu)

        # Gradient Buttons
        self.gbtn1 = gbtn.GradientButton(self,
                                         label="GradientBtn")
        self.gbtn2 = gbtn.GradientButton(self,
                                         label="GradientBmp",
                                         bitmap=bmp)

        # Layout
        vsizer = wx.BoxSizer(wx.VERTICAL)
        vsizer.Add(self.toggle, 0, wx.ALL, 12)
        vsizer.Add(self.bmpbtn, 0, wx.ALL, 12)
        hsizer1 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer1.AddMany([(self.pbtn1, 0, wx.ALL, 5),
                        (self.pbtn2, 0, wx.ALL, 5),
                        (self.pbtn3, 0, wx.ALL, 5),
                        (self.pbtn4, 0, wx.ALL, 5)])
        vsizer.Add(hsizer1, 0, wx.ALL, 12)
        hsizer2 = wx.BoxSizer(wx.HORIZONTAL)
        hsizer2.AddMany([(self.gbtn1, 0, wx.ALL, 5),
                         (self.gbtn2, 0, wx.ALL, 5)])
        vsizer.Add(hsizer2, 0, wx.ALL, 12)
        self.SetSizer(vsizer)

此代码生成以下窗口:

如何做...

它是如何工作的...

这个菜谱展示了四个不同按钮类的基本用法,所以让我们逐一查看每个按钮类,看看它们能做什么。

切换按钮

ToggleButton 是 wxPython 提供的另一个原生按钮。它就像标准的 Button 一样,但提供了两种状态。当点击按钮时,按钮将从其常规状态切换到按下状态。第二次点击将再次将其切换回常规状态。

位图按钮

BitmapButton 是一个原生平台按钮,用于显示图像而不是标签文本。此按钮的使用方式与标准 Button 类似,只是它接受一个 Bitmap 作为参数,而不是标签字符串。当按钮被按下或通过鼠标与之交互时,每个状态的 Bitmap 也可以通过以下方法进行自定义:

方法 描述
SetBitmapDisabled 设置按钮禁用时显示的位图。
SetBitmapFocus 设置按钮获得键盘焦点时显示的位图。
SetBitmapHover 设置鼠标光标悬停在按钮上时显示的位图。
SetBitmapLabel 设置默认按钮(与提供给构造函数的相同)。在没有其他位图的情况下,此位图将用于所有状态。
SetBitmapSelected 设置按钮按下时使用的位图。

面板按钮

PlateButton 是由 wx.lib.platebtn 模块提供的一个由所有者绘制的按钮类。PlateButtons 是一种平面按钮控件,当鼠标悬停在其上或点击时,会改变其背景颜色。PlateButton 可以仅显示标签,仅显示 Bitmap,同时显示标签和 Bitmap,或者任何上述组合加上一个下拉 Menu

按钮的外观和感觉也可以自定义,以控制高亮颜色、文本标签颜色、按钮形状以及高亮显示的绘制方式。PB_STYLE_SQUARE样式标志将使按钮呈现正方形形状,而不是使用其默认的圆角,而PB_STYLE_GRADIENT样式标志将导致背景根据高亮颜色绘制为渐变色。除了这种可定制性之外,PlateButton还完全实现了BitmapButton API,因此它可以作为现有应用程序中BitmapButton的即插即用替代品。

GradientButton

GradientButtonPlateButton非常相似。唯一的区别在于它不是一个平面按钮,不支持下拉菜单,并且在配置渐变颜色方面更加灵活。

还有更多...

仍然有相当多的按钮实现可供选择,你可能会在你的应用程序中找到它们很有用。

通用按钮

GenericButtonswx.lib.buttons 中的一组类,它提供了一些基本的自定义按钮,以及一些自定义的本地按钮实现。这些自定义的本地按钮保持了本地按钮的外观,但解决了某些限制。例如,有 GenBitmapTextButton,它提供了一个同时支持显示标签的位图按钮,以及 GenBitmapToggleButton,它允许创建一个显示 Bitmap 的切换按钮。

AquaButton

AquaButtons 是一个由所有者绘制的按钮类,具有类似玻璃外观,近似于原生 Macintosh Aqua 按钮的外观和感觉。由于该类由所有者绘制,它将在所有平台上提供相同的外观和感觉。此类可以在 wx.lib.agw.aquabutton 中找到。

参见

  • 本章中的 创建股票按钮 菜单展示了如何创建标准按钮。

提供带有复选框的选项

CheckBox 是一种常见的基本控件,允许用户根据 CheckBox 的样式选择两种或三种状态之一,尽管它通常只与 TrueFalse 状态相关联。在本教程中,我们将探讨如何使用 CheckBox 控件。

如何做到这一点...

要了解CheckBoxes如何工作,我们将创建一个包含两种不同类型CheckBoxes的小窗口:

class CheckBoxFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(CheckBoxFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = wx.Panel(self)
        self.checkbox1 = wx.CheckBox(self.panel,
                                     label="2 State CheckBox")
        style = wx.CHK_3STATE|wx.CHK_ALLOW_3RD_STATE_FOR_USER
        self.checkbox2 = wx.CheckBox(self.panel,
                                     label="3 State CheckBox",
                                     style=style)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.checkbox1, 0, wx.ALL, 15)
        sizer.Add(self.checkbox2, 0, wx.ALL, 15)
        self.panel.SetSizer(sizer)
        self.CreateStatusBar()

        # Event Handlers
        self.Bind(wx.EVT_CHECKBOX, self.OnCheck)

    def OnCheck(self, event):
        e_obj = event.GetEventObject()
        if e_obj == self.checkbox1:
            checked = self.checkbox1.GetValue()
            msg = "Two State Clicked: %s" % checked
            self.PushStatusText(msg)
        elif e_obj == self.checkbox2:
            state = self.checkbox2.Get3StateValue()
            msg = "Three State Clicked: %d" % state
            self.PushStatusText(msg)
        else:
            event.Skip()

它是如何工作的...

我们创建了两个 CheckBoxes;第一个是标准的双态 CheckBox,第二个是一个三态 CheckBox。双态 CheckBox 的状态可以通过其 GetValueSetValue 方法进行程序控制。

三状态复选框是通过指定两个样式标志 CHK_3STATECHK_ALLOW_3RD_STATE_FOR_USER 来创建的。如果您想限制用户无法设置不确定状态,从而只能通过程序来设置,则可以省略第二个样式标志。三状态复选框使用 Get3StateValueSet3StateValue 方法,以下列值来程序化控制 CheckBox 的状态:

  • wx.CHK_CHECKED

  • wx.CHK_UNCHECKED

  • wx.CHK_UNDETERMINED

参见

  • 第七章中的使用 BoxSizer 布局配方,窗口布局与设计展示了如何使用BoxSizer类来控制布局。

使用 TextCtrl

TextCtrl 是允许用户将文本数据输入到应用程序中的基本手段。这个控件有许多可能的用途和操作模式。本食谱将展示如何创建一个简单的登录对话框,该对话框使用两个 TextCtrls 来提供登录名和密码的输入字段。

如何做到这一点...

首先,让我们创建一个Dialog类,它将包含其他控件:

class LoginDialog(wx.Dialog):LoginDialog(wx.Dialog):
    def __init__(self, *args, **kwargs):
        super(LoginDialog, self).__init__(*args, **kwargs)wx.Dialog.__init__(self, *args, **kwargs)

        # Attributes
        self.panel = LoginPanel(self)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize()

    def GetUsername(self):
        return self.panel.GetUsername()

    def GetPassword(self):
        return self.panel.GetPassword()

接下来让我们创建一个Panel,它将包含用于用户输入登录信息的TextCtlr控件:

class LoginPanel(wx.Panel):
    def __init__(self, parent):
        super(LoginPanel, self).__init__(parent)

        # Attributes
        self._username = wx.TextCtrl(self)
        self._passwd = wx.TextCtrl(self, style=wx.TE_PASSWORD)

        # Layout
        sizer = wx.FlexGridSizer(2, 2, 8, 8)
        sizer.Add(wx.StaticText(self, label="Username:"),
                  0, wx.ALIGN_CENTER_VERTICAL)
        sizer.Add(self._username, 0, wx.EXPAND)
        sizer.Add(wx.StaticText(self, label="Password:"),
                  0, wx.ALIGN_CENTER_VERTICAL)
        sizer.Add(self._passwd, 0, wx.EXPAND)
        msizer = wx.BoxSizer(wx.VERTICAL)
        msizer.Add(sizer, 1, wx.EXPAND|wx.ALL, 20)
        btnszr = wx.StdDialogButtonSizer()
        button = wx.Button(self, wx.ID_OK)
        button.SetDefault()
        btnszr.AddButton(button)
        msizer.Add(btnszr, 0, wx.ALIGN_CENTER|wx.ALL, 12)
        btnszr.Realize()

        self.SetSizer(msizer)

    def GetUsername(self):
        return self._username.GetValue()

    def GetPassword(self):
        return self._passwd.GetValue()

如何做...

它是如何工作的...

在之前的代码中,我们做了很多事情,但由于本菜谱的重点在于TextCtrl对象,让我们先来看看我们创建的两个TextCtrl对象。

用户名的第一个文本控件只是一个使用所有默认参数创建的默认TextCtrl。默认情况下,一个TextCtrl对象被创建为一个单行控件。这仅仅创建了一个简单的文本框,用户可以输入任意数量的字符。

第二种文本控制使用特殊的 TE_PASSWORD 样式标志。这会创建一个 TextCtrl,它会用星号字符隐藏其输入,就像你在大多数应用程序或网站中的任何密码输入字段中看到的那样。当用户在此控件中输入时,每个输入的字符都会显示为星号,但实际的字符值由控件内部存储,可以通过 GetValue 方法访问。

此对话框应以ShowModal方式显示,当ShowModal返回时,您可以通过使用访问器方法GetUsernameGetPassword来检索值,以便执行登录验证。

还有更多...

TextCtrl 类提供了一系列样式标志,可以在其构造函数中提供以修改其在不同用例中的行为。以下列出了最常用的样式标志及其功能的描述。其余的可以在 wxPython 的在线 API 文档中找到(wxpython.org/docs/api/)。

样式标志 描述
wx.TE_PROCESS_ENTER 将在按下 Enter 键时使控件生成一个 wx.EVT_COMMAND_TEXT_ENTER 事件。
wx.TE_PROCESS_TAB 允许在按下 Tab 键时发出一个 wx.EVT_CHAR 事件。如果没有设置此样式,Tab 键将允许用户切换到窗口中的下一个控件。
wx.TE_MULTILINE 允许 TextCtrl 有多行。
wx.TE_READONLY 使控件为只读,用户无法向其中输入文本。
wx.TE_RICH2 使用控件的RichText版本。(仅适用于 Windows)。
wx.TE_LEFT 将所有文本对齐到控件左侧。
wx.TE_CENTER 将所有文本对齐到控件中心
wx.TE_RIGHT 将所有文本对齐到控件右侧。

参见

  • 第二章中的使用验证器验证输入配方展示了如何使用验证器来验证用户输入。

  • 使用 BoxSizer 布局中的使用 BoxSizer配方展示了如何使用 BoxSizer 类来控制布局

使用选择控件提供选项

选择控件是一种允许用户从可能的选项列表中做出单一选择的手段。它通过显示当前选中的选项,并在用户点击控件时弹出一个包含其他可能选项的列表来实现这一点。这使得它在屏幕空间的使用上非常高效。

如何做到这一点...

要了解Choice控件的工作原理,我们将创建一个简单的Panel,其中包含一个具有三个选项的Choice控件:

class ChoicePanel(wx.Panel):
    def __init__(self, parent):
        super(ChoicePanel, self).__init__(parent)

        # Attributes
        items = ["item 1", "item 2", "item 3"]
        self.choice = wx.Choice(self, choices=items)
        self.choice.SetSelection(0)

        # Layout
        sizer = wx.BoxSizer()
        sizer.Add(self.choice, 1,
                  wx.EXPAND|wx.ALL, 20)
        self.SetSizer(sizer)

        # Event Handlers
        self.Bind(wx.EVT_CHOICE, self.OnChoice)

    def OnChoice(self, event):
        selection = self.choice.GetStringSelection()
        index = self.choice.GetSelection()
        print "Selected Item: %d '%s'" % (index, selection)

它是如何工作的...

Choice 控件管理一个字符串列表。该控件包含的字符串列表可以通过构造函数指定,或者通过调用带有要放入控件的字符串列表的 SetItems 方法来指定。当点击时,控件将显示一个包含所有字符串的弹出列表。用户做出选择后,将触发一个 EVT_CHOICE 事件。

注意事项

平台通知:在 Windows 中,Choice 控件在创建时不会自动选择其第一个项目。由于这种不一致性,有时在创建控件后显式设置选择是可取的,正如我们在本例中所做的那样,以确保跨平台行为的一致性。

还有更多...

控制项创建后,可以使用以下方法进行操作或更改:

方法 描述
Append 将字符串添加到由控制器管理的列表末尾
AppendItems 将字符串列表追加到由控件管理的列表中
插入 将字符串插入由控件管理的列表中
SetItems 设置控制显示的字符串列表

添加菜单和菜单栏

大多数应用程序都有菜单。菜单是提供给应用程序用户执行操作的一种方式,可以通过点击它们或使用与每个菜单项关联的键盘快捷键来实现。应用程序的菜单由三个组件组成:MenuBar(菜单栏)、Menus(菜单)和MenuItems(菜单项)。MenuBar包含Menus,而Menus包含MenuItems。本教程将展示如何向Frame添加一个包含一些菜单的MenuBar

如何做到这一点...

在这里,我们将创建一个具有一些菜单选项的Frame,用于控制TextCtrl:中的操作:

ID_READ_ONLY = wx.NewId()

class MenuFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MenuFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = wx.Panel(self)
        self.txtctrl = wx.TextCtrl(self.panel,
                                   style=wx.TE_MULTILINE)

        # Layout
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.txtctrl, 1, wx.EXPAND)
        self.panel.SetSizer(sizer)
        self.CreateStatusBar() # For output display

        # Setup the Menu
        menub = wx.MenuBar()

        # File Menu
        filem = wx.Menu()
        filem.Append(wx.ID_OPEN, "Open\tCtrl+O")
        menub.Append(filem, "&File")

        # Edit Menu
        editm = wx.Menu()
        editm.Append(wx.ID_COPY, "Copy\tCtrl+C")
        editm.Append(wx.ID_CUT, "Cut\tCtrl+X")
        editm.Append(wx.ID_PASTE, "Paste\tCtrl+V")
        editm.AppendSeparator()
        editm.Append(ID_READ_ONLY, "Read Only",
                     kind=wx.ITEM_CHECK)
        menub.Append(editm, "E&dit")
        self.SetMenuBar(menub)

        # Event Handlers
        self.Bind(wx.EVT_MENU, self.OnMenu)

    def OnMenu(self, event):
        """Handle menu clicks"""
        evt_id = event.GetId()
        actions = { wx.ID_COPY  : self.txtctrl.Copy,
                    wx.ID_CUT   : self.txtctrl.Cut,
                    wx.ID_PASTE : self.txtctrl.Paste }
        action = actions.get(evt_id, None)
        if action:
            action()
        elif evt_id == ID_READ_ONLY:
            # Toggle enabled state
            self.txtctrl.Enable(not self.txtctrl.Enabled)
        elif evt_id == wx.ID_OPEN:
            dlg = wx.FileDialog(self, "Open File", 
                                style=wx.FD_OPEN)
            if dlg.ShowModal() == wx.ID_OK:
                fname = dlg.GetPath()
                handle = open(fname, 'r')
                self.txtctrl.SetValue(handle.read())
                handle.close()
        else:
            event.Skip()

它是如何工作的...

首先需要查看的是我们创建了哪个 MenuBar 对象。MenuBar 是我们将所有 Menus 附着的对象,它最终将负责管理它们。接下来,我们开始创建我们的 Menus,这是一个相当直接的过程。需要做的只是为每个我们希望添加到 Menu 中的新项目调用 Append 方法。

Append 函数接受几个参数,但需要注意的重要参数是标签参数。我们传递的字符串中可以包含一些特殊的格式化选项,用于为 MenuItem 设置键盘快捷键。在标签中放置一个字母前的 '&' 将设置一个键盘助记符,允许通过键盘导航到该项目。然而,更重要的是,放置一个 Tab 字符 (\t) 后跟一个快捷键选项 Ctrl + C 将设置一个键盘快捷键来选择菜单选项,并导致生成一个 EVT_MENU 事件。

注意事项

平台通知:在 OS X 操作系统中,Ctrl 键字将自动转换为 Apple/Command 键。

最后,我们只需在我们的MenuBar上调用Append方法,以便将我们创建的每个Menus添加到其中,然后最终在Frame上调用SetMenuBar方法,将MenuBar添加到我们的Frame中。

还有更多...

菜单还有一些我们上面没有提到的附加功能。以下是一些关于你可以使用菜单做更多事情的参考。

子菜单

菜单可以通过AppendMenu函数添加子菜单。

自定义菜单项

当在Menu上调用Append方法时,会创建MenuItemsAppend方法接受一个名为"kind"的关键字参数,它可以接受以下任何值:

描述
wx.ITEM_NORMAL 默认值
wx.ITEM_SEPARATOR 创建一个分隔符项。直接调用 AppendSeparator 比这样做更简单。
wx.ITEM_CHECK Menu 添加一个 CheckBox
wx.ITEM_RADIO Menu 添加一个 RadioButton

MenuItems也可以通过在Menu对象的Append方法返回的MenuItem对象上调用SetBitmap方法来添加Bitmaps

注意事项

平台通知:在 Linux/GTK 上,使用库存 ID 的 MenuItems 将自动获得与它们关联的系统主题提供的位图。

参见

  • 第一章中的使用库存 ID配方,在使用 wxPython 入门一书中讨论了内置标准控件 ID 的使用。

  • 第二章中的使用 UpdateUI 事件配方,在响应事件一节中讨论了如何使用UpdateUI事件来管理 UI 的状态。

使用工具栏

工具栏菜单有很多相似之处,它们都提供了一种将界面中的操作与应用程序中的操作相连接的方式。它们的不同之处在于工具栏使用图像来表示操作,并且必须直接点击以启动操作。它们为用户提供了一个简单直观的点选界面。本食谱将展示一个自定义的工具栏类,该类会自动从系统的ArtProvider获取位图。

如何做到这一点...

让我们先定义我们的自定义ToolBar类,然后映射一些股票 ID 到艺术资源 ID:

ART_MAP = { wx.ID_CUT : wx.ART_CUT,
            wx.ID_COPY : wx.ART_COPY,
            wx.ID_PASTE : wx.ART_PASTE }

class EasyToolBar(wx.ToolBar):
    def AddEasyTool(self, id, shortHelp=u"", longHelp=u""):
        """Simplifies adding a tool to the toolbar
        @param id: Stock ID

        """
        assert id in ART_MAP, "Unknown Stock ID"
        art_id = ART_MAP.get(id)
        bmp = wx.ArtProvider.GetBitmap(art_id, wx.ART_TOOLBAR)
        self.AddSimpleTool(id, bmp, shortHelp, longHelp)

现在我们可以将这个自定义的 ToolBar 类用于任何我们需要 ToolBar 的地方。以下是一个创建包含三个项目的 EasyToolBar 的最小示例代码片段:

class ToolBarFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(ToolBarFrame, self).__init__(*args, **kwargs)

        # Setup the ToolBar
        toolb = EasyToolBar(self)
        toolb.AddEasyTool(wx.ID_CUT)
        toolb.AddEasyTool(wx.ID_COPY)
        toolb.AddEasyTool(wx.ID_PASTE)
        toolb.Realize()
        self.SetToolBar(toolb)

        # Event Handlers
        self.Bind(wx.EVT_TOOL, self.OnToolBar)

    def OnToolBar(self, event):
        print "ToolBarItem Clicked", event.GetId()

它是如何工作的...

EasyToolBar 类利用了一个股票 ID 到艺术资源 ID 的映射。当调用 AddEasyTool 方法时,它将在系统的艺术提供者中查找该艺术资源。这大大简化了 ToolBar 的使用,因为我们不需要每次添加工具时都重复编写获取适当位图的代码。

ToolBarFrame 类展示了如何使用 EasyToolBar 的一个示例。使用 ToolBar 可以概括为四个步骤。首先,创建 ToolBar,其次添加工具,然后调用 Realize 方法来告知 ToolBar 所有工具已经添加完毕,最后,调用 FrameSetToolBar 方法以将 ToolBar 添加到 Frame 中。

更多内容...

工具栏样式

有许多样式标志可以传递给ToolBars构造函数以修改其外观和行为。以下是其中一些更有用的列表:

样式标志 描述
wx.TB_DOCKABLE 允许 ToolBarFrame 中解耦(仅限 GTK)
wx.TB_FLAT 使 ToolBar 看起来更扁平(仅限 MSW 和 GTK)
wx.TB_HORIZONTAL 水平工具布局
wx.TB_VERTICAL 垂直工具布局
wx.TB_TEXT 在工具图标下方显示标签
wx.TB_NO_TOOLTIPS 当工具被悬停时不要显示 ToolTips
wx.TB_BOTTOM ToolBar 放置在父窗口的底部
wx.TB_RIGHT ToolBar 放置在父窗口的右侧

其他类型的工具

除了标准的图标工具外,还可以向ToolBar添加不同类型的工具或控件。以下是一些其他ToolBar方法的快速参考。

工具栏方法 描述
AddControl 允许将如 Button 这样的控件添加到 ToolBar 中。
AddCheckLabelTool 添加一个可切换的工具。
AddRadioLabelTool 添加一个将像 RadioButton 一样工作的工具。
AddSeparator ToolBar 中添加一条垂直线以分隔项目。

事件

ToolBar 工具在点击时会触发一个 EVT_TOOL 事件。如果你已经有一个与相同 ID 绑定到 EVT_MENU 事件处理器的 MenuItem,那么就不需要为工具事件创建单独的事件处理器。系统会自动将工具事件路由到你的菜单处理器。

参见

  • 本章中关于添加菜单和菜单栏的配方讨论了菜单和菜单事件的使用,这些与工具栏密切相关。

  • 在第十章的自定义 ArtProvider配方中,创建组件和扩展功能包括更多关于检索位图资源的示例和信息。

如何使用弹出菜单

弹出菜单(也称为上下文菜单)是在用户右击控件或窗口的一部分时提供上下文相关操作访问的有用方式。弹出菜单的工作方式与常规菜单相同,但需要一些特殊处理,因为没有MenuBar来管理它们。本食谱将创建一个混合类来帮助管理弹出菜单。

如何做到这一点...

在这里,我们将定义一个混合类来管理上下文菜单的创建和生命周期:

class PopupMenuMixin(object):
    def __init__(self):
        super(PopupMenuMixin, self).__init__()

        # Attributes
        self._menu = None

        # Event Handlers
        self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu)

    def OnContextMenu(self, event):
        """Creates and shows the Menu"""
        if self._menu is not None:
            self._menu.Destroy()

        self._menu = wx.Menu()
        self.CreateContextMenu(self._menu)
        self.PopupMenu(self._menu)

    def CreateContextMenu(self, menu):
        """Override in subclass to create the menu"""
        raise NotImplementedError

它是如何工作的...

这个小巧的混合类非常通用,可以与任何类型的窗口子类一起使用,以添加自定义上下文菜单支持。使用此混合类的子类必须重写CreateContextMenu方法来创建自己的Menu,然后混合类将处理其余部分。以下是一个使用PopupMenuMixin类的最小示例。它将创建一个带有三个项目的上下文菜单的Panel;更完整的示例包含在伴随此主题的示例代码中。

class PanelWithMenu(wx.Panel, PopupMenuMixin):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        PopupMenuMixin.__init__(self)

    def CreateContextMenu(self, menu):
        """PopupMenuMixin Implementation"""
        menu.Append(wx.ID_CUT)
        menu.Append(wx.ID_COPY)
        menu.Append(wx.ID_PASTE)

当用户右键点击或从键盘发起上下文菜单时,会触发 EVT_CONTEXT_MENU 事件。因为上下文菜单可以通过多种方式显示,所以使用 EVT_CONTEXT_MENU 而不是使用鼠标右键事件是很重要的。我们的混合类将捕获此事件,并首先清理任何现有的 Menu。由于弹出菜单没有 MenuBar 来管理它们,因此我们需要自己清理它们,否则,如果它们没有被销毁,可能会导致内存泄漏。接下来,将调用子类的 CreateContextMenu 方法来向 Menu 添加项目。最后,我们通过调用 PopupMenu 方法来显示 Menu

当用户点击菜单中的项目时,将向属于弹出菜单的窗口发送一个EVT_MENU事件。因此,有必要绑定你自己的菜单处理程序来处理MenuEvents

参见

  • 本章中的 添加菜单和菜单栏 菜单展示了如何创建菜单对象。

  • 第九章中的使用混合类配方,设计方法和技巧讨论了如何使用混合类。

使用 StaticBox 对控件进行分组

StaticBox 是一个相对简单的控件,用于将其他相关控件组合在一起,通过围绕它们绘制一个可选包含标签的边框来实现。然而,由于它与包含的控件之间的关系,StaticBox 控件的用法与其他控件略有不同。因此,这个菜谱将展示如何使用 StaticBox,并解释一些它的特性。

如何做到这一点...

要了解如何向一个StaticBox添加控件,让我们创建一个包含StaticBoxPanel类,并向其添加一些控件:

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super(MyPanel, self).__init__(parent)

        # Layout
        sbox = wx.StaticBox(self, label="Box Label")
        sboxsz = wx.StaticBoxSizer(sbox, wx.VERTICAL)

        # Add some controls to the box
        cb = wx.CheckBox(self, label="Enable")
        sboxsz.Add(cb, 0, wx.ALL, 8)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(wx.StaticText(self, label="Value:"))
        sizer.Add((5, 5))
        sizer.Add(wx.TextCtrl(self))
        sboxsz.Add(sizer, 0, wx.ALL, 8)8)

        msizer = wx.BoxSizer(wx.VERTICAL)
        msizer.Add(sboxsz, 0, wx.EXPAND|wx.ALL, 20)
        self.SetSizer(msizer)

它是如何工作的...

即使StaticBox是其他控件的容器,但实际上它是它所包含的控件的兄弟,而不是父窗口。在使用StaticBox时需要记住的最重要的一点是,它必须在它将包含的任何控件之前创建。如果它在其兄弟控件之前没有创建,那么它们在处理鼠标事件时将会有问题。

StaticBox 使用 StaticBoxSizer 来将控件添加到框中,同时管理其大小并在其中定位控件。StaticBoxSizer 的使用方法与常规的 BoxSizer 在所有方面都相同,只是其构造函数接受一个 StaticBox 作为第一个参数。调用 StaticBoxSizerAdd 方法用于将控件添加到 StaticBox 中。与 BoxSizer 类似,StaticBoxSizerAdd 方法将待添加的对象作为第一个参数,然后可选地包括比例、布局标志和边框关键字参数。

参见

  • 在第七章的使用一个BoxSizer的食谱中,窗口布局与设计包含了更多基于 sizer 的控件布局示例。

第四章:用户界面的高级构建块

在本章中,我们将涵盖:

  • 使用ListCtrl列出数据

  • 使用 CustomTreeCtrl 浏览文件

  • 创建一个 VListBox

  • 使用词法分析器创建 StyledTextCtrl

  • 使用托盘图标

  • 笔记本中添加标签页

  • 使用FlatNotebook

  • 使用ScrolledPanel进行滚动

  • 简化 FoldPanelBar

简介

展示数据集合和管理复杂的窗口布局是大多数 UI 开发者迟早会遇到的任务。wxPython 提供了一系列组件,以帮助开发者满足这些更高级界面的需求。

随着应用程序在其用户界面中需要显示的控制和数据量增加,有效管理可用屏幕空间的任务也随之增加。要将这些信息适当地放入可用空间,需要使用一些更高级的控制和容器;因此,让我们深入探讨,开始我们对于 wxPython 所能提供的某些更高级控制器的探索之旅。

使用 ListCtrl 列出数据

ListCtrl 是一种用于显示文本和/或图像集合的多功能控件。该控件支持许多不同的显示格式,尽管通常其最常用的显示模式是报表模式。报表模式具有与网格或电子表格非常相似的视觉表示,因为它可以有多个行和列,以及列标题。本食谱展示了如何从在报表模式下创建的 ListCtrl 中填充和检索数据。

使用 ListCtrl 列出数据

如何做到这一点...

ListCtrl 的设置比大多数基本控件要复杂一些,因此我们将首先创建一个子类来设置我们希望在控件中拥有的列:

class MyListCtrl(wx.ListCtrl):
    def __init__(self, parent):
        super(MyListCtrl, self).__init__(parent,
                                         style=wx.LC_REPORT)

        # Add three columns to the list          
        self.InsertColumn(0, "Column 1")
        self.InsertColumn(1, "Column 2")
        self.InsertColumn(2, "Column 3")

    def PopulateList(self, data):
        """Populate the list with the set of data. Data
        should be a list of tuples that have a value for each
        column in the list.
        [('hello', 'list', 'control'),]
        """
        for item in data:
            self.Append(item)

接下来我们将创建一个我们的ListCtrl实例,并将其放置在一个Panel上,然后使用我们的PopulateList方法将一些示例数据放入控件中:

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super(MyPanel, self).__init__(parent)

        # Attributes
        self.lst = MyListCtrl(self)

        # Setup
        data = [ ("row %d" % x,
                  "value %d" % x,
                  "data %d" % x) for x in range(10) ]
        self.lst.PopulateList(data)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.lst, 1, wx.EXPAND)
        self.SetSizer(sizer)

        # Event Handlers
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, 
                  self.OnItemSelected)

    def OnItemSelected(self, event):
        selected_row = event.GetIndex()
        val = list()
        for column in range(3):
            item = self.lst.GetItem(selected_row, column)
            val.append(item.GetText())
        # Show what was selected in the frames status bar
        frame = self.GetTopLevelParent()
        frame.PushStatusText(",".join(val))

它是如何工作的...

通常在使用ListCtrl时,需要进行相当多的设置,因此将控制器的使用封装在一个专门的子类中而不是直接使用它是个不错的选择。在我们的ListCtrl类中,我们保持了相当基础的设计。我们只是使用了InsertColumn方法来设置列表的三个列。然后为了方便,添加了PopulateList方法,允许从 Python 数据列表中填充ListCtrl。它简单地封装了ListCtrlAppend方法,该方法只需一个可迭代的字符串,每个字符串代表列表中的一个列。

MyPanel 类旨在展示如何使用我们创建的 ListCtrl 类。首先,我们通过生成一个元组列表并调用我们的 PopulateList 方法来填充它。为了展示如何从列表中检索数据,我们创建了一个事件处理程序用于 EVT_LIST_ITEM_SELECTED,它将在控制中每次进行新选择时被触发。为了从 ListCtrl 中检索一个值,你需要知道你希望从中检索数据的单元格的行和列索引,然后调用 GetItem 方法并传入行和列以获取表示该单元格的 ListItem 对象。然后可以通过调用 ListItemGetText 方法来检索单元格的字符串值。

还有更多...

根据用于创建 ListCtrl 的样式标志,它将以许多不同的可能方式表现。正因为如此,了解一些可以用于创建 ListCtrl 的不同样式标志是很重要的。

样式标志 描述
LC_LIST 在列表模式下,控件将自动计算列,因此无需调用 InsertColumn。它可以用来显示字符串,以及可选的小图标
LC_REPORT 单列或多列报表视图,可以带或不带标题显示
LC_ICON 大图标视图,可选带有标签
LC_SMALL_ICON 可选带有标签的小图标视图
LC_EDIT_LABELS 允许用户编辑项目标签
LC_NO_HEADER 隐藏列标题(报告模式)
LC_SORT_ASCENDING 按升序排序项目(必须提供SortItems回调方法)
LC_SORT_DESCENDING 按降序排序项目(必须提供SortItems回调方法)
LC_HRULE 在行之间绘制水平线(报告模式)
LC_VRULE 在列之间绘制垂直线(报告模式)
LC_SINGLE_SEL 仅允许一次选择一个项目(默认允许多选)
LC_VIRTUAL 按需获取用于在列表中显示的项目(报告模式)

虚拟模式

当在虚拟模式下创建ListCtrl(使用LC_VIRTUAL样式标志)时,它不会内部存储数据;相反,当需要显示数据时,它会从数据源请求数据。这种模式在您拥有大量数据时非常有用,因为预先在控件中加载这些数据可能会引起性能问题。要使用虚拟模式的ListCtrl,您必须调用SetItemCount来告诉控件有多少行数据,并重写OnGetItemText方法,以便在控件请求时返回ListItem的文本。

参见

  • 本章中创建 创建 VListBox 菜单的示例是另一个用于以列表形式展示数据的控件示例。

使用 CustomTreeCtrl 浏览文件

TreeCtrl 是在用户界面中显示层次数据的一种方式。CustomTreeCtrl 是一个完全由所有者绘制的 TreeCtrl,其外观和功能与默认的 TreeCtrl 几乎相同,但它提供了一些默认原生控件所不具备的额外功能和可定制性。本食谱展示了如何通过使用 CustomTreeCtrl 来创建一个自定义文件浏览器类。

如何做到这一点...

要创建这个自定义的FileBrowser控件,我们将使用其构造函数来设置用于树中文件夹和文件的图片:

import  os
import wx
import wx.lib.customtreectrl as customtree

class FileBrowser(customtree.CustomTreeCtrl):
    FOLDER, \
    ERROR, \
    FILE = range(3)
    def __init__(self, parent, rootdir, *args, **kwargs):
        super(FileBrowser, self).__init__(parent,
                                          *args,
                                          **kwargs)         
        assert os.path.exists(rootdir), \
               "Invalid Root Directory!"
        assert os.path.isdir(rootdir), \
               "rootdir must be a Directory!"

        # Attributes
        self._il = wx.ImageList(16, 16)
        self._root = rootdir
        self._rnode = None  

        # Setup
        for art in (wx.ART_FOLDER, wx.ART_ERROR,
                    wx.ART_NORMAL_FILE):
            bmp = wx.ArtProvider.GetBitmap(art, size=(16,16))
            self._il.Add(bmp)
        self.SetImageList(self._il)
        self._rnode = self.AddRoot(os.path.basename(rootdir),
                                   image=FileBrowser.FOLDER,
                                   data=self._root)
        self.SetItemHasChildren(self._rnode, True)
        # use Windows-Vista-style selections       
        self.EnableSelectionVista(True)

        # Event Handlers
        self.Bind(wx.EVT_TREE_ITEM_EXPANDING, 
                  self.OnExpanding)
        self.Bind(wx.EVT_TREE_ITEM_COLLAPSED, 
                  self.OnCollapsed)

    def _GetFiles(self, path):
        try:
            files = [fname for fname in os.listdir(path)
                     if fname not in ('.', '..')]
        except OSError:
            files = None
        return files

以下两个事件处理器用于更新在树中节点展开或折叠时显示哪些文件:

    def OnCollapsed(self, event):
        item = event.GetItem()
        self.DeleteChildren(item)

    def OnExpanding(self, event):
        item = event.GetItem()
        path = self.GetPyData(item)
        files = self._GetFiles(path)

        # Handle Access Errors
        if files is None:
            self.SetItemImage(item, FileBrowser.ERROR)
            self.SetItemHasChildren(item, False)
            return

        for fname in files:
            fullpath = os.path.join(path, fname)
            if os.path.isdir(fullpath):
                self.AppendDir(item, fullpath)
            else:
                self.AppendFile(item, fullpath)

以下方法被添加为 API,用于与控件一起添加项目并检索它们的磁盘路径:

    def AppendDir(self, item, path):
        """Add a directory node"""
        assert os.path.isdir(path), "Not a valid directory!"
        name = os.path.basename(path)
        nitem = self.AppendItem(item, name,
                                image=FileBrowser.FOLDER,
                                data=path)
        self.SetItemHasChildren(nitem, True)

    def AppendFile(self, item, path):
        """Add a file to a node"""
        assert os.path.isfile(path), "Not a valid file!"
        name = os.path.basename(path)
        self.AppendItem(item, name,
                        image=FileBrowser.FILE,
                        data=path)

    def GetSelectedPath(self):
        """Get the selected path"""
        sel = self.GetSelection()
        path = self.GetItemPyData(sel)
        return path

    def GetSelectedPaths(self):
        """Get a list of selected paths"""
        sels = self.GetSelections()
        paths = [self.GetItemPyData(sel)
                 for sel in sels ]
        return paths

它是如何工作的...

只需几行代码,我们就创建了一个非常实用的迷你小工具,用于显示和操作文件系统。让我们快速了解一下它是如何工作的。

在类的构造函数中,我们使用控制器的 AddRoot 方法添加了一个根节点。根节点是一个顶级节点,其上方没有其他父节点。第一个参数是要显示的文本,image 参数指定了 TreeItem 的默认图像,而 data 参数指定了与项目关联的任何类型的数据——在这种情况下,我们为项目的路径设置了一个字符串。然后我们为该项目调用了 SetItemHasChildren 方法,以便它旁边会显示一个按钮,允许它被展开。在构造函数中我们做的最后一件事是将控制器绑定到两个事件上,这样我们就可以在其中一个节点被展开或折叠时更新树。

在节点即将展开之前,我们的EVT_TREE_ITEM_EXPANDING处理程序将被调用。正是在这里,我们找到目录节点下的所有文件和文件夹,然后通过调用AppendItem将它们作为该节点的子项添加,AppendItem的功能类似于AddRoot,但用于向树中已存在的节点添加项。

相反,当树中的节点即将被折叠时,我们的EVT_TREE_ITEM_COLLAPED事件处理程序将被调用。在这里,我们只是简单地调用DeleteChildren来从节点中删除子项,这样我们就可以在节点下一次展开时更容易地更新它们。否则,我们下次展开时将不得不找出有什么不同,然后删除已删除的项,并插入可能已添加到目录中的新项。

我们班级的最后两项是为了获取所选项目的文件路径,由于我们在每个节点中存储文件路径,所以这仅仅是一个通过调用GetPyData从当前选中的每个TreeItems获取数据的问题。

还有更多...

在这个菜谱中我们做的绝大多数事情实际上也可以用标准的 TreeCtrl 来实现。区别在于 CustomTreeCtrl 提供的额外可定制性数量。由于它是一个完全由所有者绘制的控件,几乎所有的可见属性都可以进行定制。以下是可用于定制其外观的一些函数列表:

函数 描述
EnableSelectionGradient(bool) 使用渐变来绘制树项选择矩形。
EnableSelectionVista(bool) 使用类似于 Windows Vista 中看到的原生控件的美观圆角矩形来进行项目选择。
SetButtonsImageList(ImageList) 更改展开/折叠按钮。ImageList 应包含以下状态的四个位图,顺序如下:正常,选中,展开,以及展开选中
SetConnectionPen(pen) 改变树中项目之间连接线的绘制方式。接受一个用于绘制线的 wx.Pen 对象。
SetBackgroundImage(bitmap) 允许使用图像作为控件背景。
SetBackgroundColour(colour) 用于更改控件背景颜色。

创建一个 VListBox

VListBox 控件与 ListBox 控件非常相似,但它却是虚拟的(它不内部存储数据)并且允许项目具有可变的行高。它通过提供一系列虚拟回调方法来实现,你必须在一个子类中重写这些方法以按需绘制项目。由于需要重写纯虚拟方法,VListBox 将始终被派生。这个配方展示了如何创建一个派生的 VListBox 控件,它支持每个项目中的图标和文本。

如何做到这一点...

要创建我们的用户列表控件,我们只需继承一个VListBox并重写其中的一些回调方法以执行必要的操作:

class UserListBox(wx.VListBox):
    """Simple List Box control to show a list of users"""
    def __init__(self, parent, users):
        """@param users: list of user names"""
        super(UserListBox, self).__init__(parent)

        # Attributes
        # system-users.png is a sample image provided with
        # this chapters sample code.
        self.bmp = wx.Bitmap("system-users.png",
                             wx.BITMAP_TYPE_PNG)
        self.bh = self.bmp.GetHeight()
        self.users = users

        # Setup
        self.SetItemCount(len(self.users))

    def OnMeasureItem(self, index):
        """Called to get an items height"""
        # All our items are the same so index is ignored
        return self.bh + 4

    def OnDrawSeparator(self, dc, rect, index):
        """Called to draw the item separator"""
        oldpen = dc.GetPen()
        dc.SetPen(wx.Pen(wx.BLACK))
        dc.DrawLine(rect.x, rect.y,
                    rect.x + rect.width,
                    rect.y)
        rect.Deflate(0, 2)
        dc.SetPen(oldpen)

    def OnDrawItem(self, dc, rect, index):
        """Called to draw the item"""
        # Draw the bitmap
        dc.DrawBitmap(self.bmp, rect.x + 2,
                      ((rect.height - self.bh) / 2) + rect.y)
        # Draw the label to the right of the bitmap
        textx = rect.x + 2 + self.bh + 2
        lblrect = wx.Rect(textx, rect.y,
                          rect.width - textx,
                          rect.height)
        dc.DrawLabel(self.users[index], lblrect,
                     wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL)

这里是UserListBox的截图,其中包含了一些示例数据。

如何做...

它是如何工作的...

我们的定制VListBox控件可以用于任何需要显示用户列表的应用程序。构造函数接收一个用户名列表并调用SetItemCount来告知控件需要显示的项目数量。我们还加载了一个位图,用于列表项中。此位图包含在伴随本主题的示例代码中。

从这个菜谱中,我们需要掌握的是我们覆盖的三个虚拟回调方法,以便在我们的控件中绘制项目:

  1. 第一个需要重写的方法是 OnMeasureItem。这个方法将为列表中的每个项目被调用,并且需要返回项目的高度。

  2. 下一个方法是 OnDrawSeparator。此方法为可选,可用于在控件中的每个项目之间绘制分隔符。如有必要,它还可以修改 Rect,这样当调用 OnDrawItem 时,它将知道不要在分隔符上绘制。

  3. 最终的方法是 OnDrawItem。这个方法用于绘制实际的项。对于我们的控件,我们绘制一个位图,然后将用户的姓名作为标签放置在其右侧。就是这样;很简单,对吧。

还有更多...

有几种其他方法可供使用,这些方法在实现 VListBox 子类时可能很有用。以下列表描述了这些方法。

方法 描述
OnDrawItemBackground 此方法可以像 DrawItem 一样被重写,以便为项目绘制自定义背景。默认基类会以系统默认选择颜色绘制选中项的背景。
IsSelected 此方法可用于查看项目是否被选中,如果你想在OnDrawItem中更改绘制项目的方式,例如使字体加粗。

参见

  • 第一章中的理解继承限制配方,wxPython 入门包含了关于 C++对象中虚方法的解释。

  • 本章中关于使用 ListCtrl 列表控件展示数据的示例,是另一种将数据以列表形式呈现的控件示例。

  • 第八章中的“屏幕绘图”配方,《屏幕绘图》,讨论了 PaintEvents 和设备上下文的使用。

使用词法分析器的样式文本控件

StyledTextCtrl 是由 wx.stc 模块提供的先进文本控件类。该类是对 Scintilla 源代码编辑组件的封装(参见 www.scintilla.org)。StyledTextCtrl 主要用于显示和操作各种编程语言的源代码。它为许多不同类型的源代码文件提供了内置的语法高亮支持,并且可以扩展以使用自定义词法分析器。本菜谱展示了如何设置控件以使用其内置的词法分析器对 Python 进行源代码高亮显示。

如何做到这一点...

要开始,我们将定义一个通用的语言编辑器类,该类将管理所有常见的样式设置,以便我们能够轻松地创建支持不同类型编程语言的其它类:

imp ort wx
import wx.stc as stc
import keyword

class CodeEditorBase(stc.StyledTextCtrl):
    def __init__(self, parent):
        super(CodeEditorBase, self).__init__(parent)

        # Attributes
        font = wx.Font(10, wx.FONTFAMILY_MODERN,
                           wx.FONTSTYLE_NORMAL,
                           wx.FONTWEIGHT_NORMAL)
        self.face = font.GetFaceName()
        self.size = font.GetPointSize()

        # Setup
        self.SetupBaseStyles()

    def EnableLineNumbers(self, enable=True):
        """Enable/Disable line number margin"""
        if enable:
            self.SetMarginType(1, stc.STC_MARGIN_NUMBER)
            self.SetMarginMask(1, 0)
            self.SetMarginWidth(1, 25)
        else:
            self.SetMarginWidth(1, 0)

    def GetFaces(self):
        """Get font style dictionary"""
        return dict(font=self.face,
                    size=self.size)

    def SetupBaseStyles(self):
        """Sets up the the basic non lexer specific
        styles.
        """
        faces = self.GetFaces()
        default = "face:%(font)s,size:%(size)d" % faces
        self.StyleSetSpec(stc.STC_STYLE_DEFAULT, default)
        line = "back:#C0C0C0," + default
        self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, line)
        self.StyleSetSpec(stc.STC_STYLE_CONTROLCHAR,
                          "face:%(font)s" % faces)

现在我们将从我们的CodeEditorBase类派生出一个新的类,该类专门用于处理 Python 文件:

class PythonCodeEditor(CodeEditorBase):
    def __init__(self, parent):
        super(PythonCodeEditor, self).__init__(parent)

        # Setup
        self.SetLexer(wx.stc.STC_LEX_PYTHON)
        self.SetupKeywords()
        self.SetupStyles()
        self.EnableLineNumbers(True)

    def SetupKeywords(self):
        """Sets up the lexers keywords"""
        kwlist = u" ".join(keyword.kwlist)
        self.SetKeyWords(0, kwlist)
        #self.SetKeywords(1, user_kw)

    def SetupStyles(self):
        """Sets up the lexers styles"""
        # Python styles
        faces = self.GetFaces()
        fonts = "face:%(font)s,size:%(size)d" % faces
        default = "fore:#000000," + fonts

        # Default 
        self.StyleSetSpec(stc.STC_P_DEFAULT, default)
        # Comments
        self.StyleSetSpec(stc.STC_P_COMMENTLINE,
                          "fore:#007F00," + fonts)
        # Number
        self.StyleSetSpec(stc.STC_P_NUMBER,
                          "fore:#007F7F," + fonts)
        # String
        self.StyleSetSpec(stc.STC_P_STRING,
                          "fore:#7F007F," + fonts)
        # Single quoted string
        self.StyleSetSpec(stc.STC_P_CHARACTER,
                          "fore:#7F007F," + fonts)
        # Keyword
        self.StyleSetSpec(stc.STC_P_WORD,
                          "fore:#00007F,bold," + fonts)
        # Triple quotes
        self.StyleSetSpec(stc.STC_P_TRIPLE,
                          "fore:#7F0000," + fonts)
        # Triple double quotes
        self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE,
                          "fore:#7F0000," + fonts)
        # Class name definition
        self.StyleSetSpec(stc.STC_P_CLASSNAME,
                          "fore:#0000FF,bold," + fonts)
        # Function or method name definition
        self.StyleSetSpec(stc.STC_P_DEFNAME,
                          "fore:#007F7F,bold," + fonts)
        # Operators
        self.StyleSetSpec(stc.STC_P_OPERATOR, "bold," + fonts)
        # Identifiers
        self.StyleSetSpec(stc.STC_P_IDENTIFIER, default)
        # Comment-blocks
        self.StyleSetSpec(stc.STC_P_COMMENTBLOCK,
                          "fore:#7F7F7F," + fonts)
        # End of line where string is not closed
        eol_style = "fore:#000000,back:#E0C0E0,eol," + fonts
        self.StyleSetSpec(stc.STC_P_STRINGEOL, eol_style)

它是如何工作的...

我们创建了两个类:一个基础编辑器类和一个专门用于 Python 源文件的类。让我们首先看看 CodeEditorBase 类。

CodeEditorBase 设置了控件的基本功能,它仅仅是为了封装一些常见的项目,如果我们决定稍后添加其他针对不同类型源文件的专用类,它就派上用场了。

首要的是,它初始化基本的窗口样式并提供字体信息。StyledTextCtrl 为缓冲区中不同文本的样式提供了多种样式规范。这些样式是通过使用 StyleSetSpec 方法来指定的,该方法接受样式 ID 和样式规范字符串作为参数。适用于所有词法分析器的通用样式 ID 以 STC_STYLE_ 前缀标识。样式规范字符串的格式如下:

ATTRIBUTE:VALUE,ATTRIBUTE:VALUE,MODIFIER 

在这里,ATTRIBUTEVALUE 可以替换为以下表格中可能规格的任何组合:

属性 可能的值
fore 前景色;可以是颜色名称(黑色)或十六进制颜色字符串(例如,#000000
back 背景颜色;可以是颜色名称(例如,白色)或十六进制颜色字符串(例如,#FFFFFF
face 一种字体名称(例如,Monaco
size 字体的大小(例如,10

此外,还有一些额外的 MODIFER 属性也支持不包含 VALUE 参数:

修饰符 描述
粗体 使文本加粗
斜体 斜体化文本
eol 将背景样式扩展到当前行的末尾
underline 下划线文本

StyledTextCtrl 还支持在缓冲区的左侧设置特殊边距,用于显示行号、断点和代码折叠按钮等。我们的 CodeEditorBase 通过其 EnableLineNumbers 方法展示了如何在最左侧边距中启用行号。

我们推导出的 PythonCodeEditor 类仅仅执行设置正确词法分析器所需的三个基本操作:

  1. 首先,它调用SetLexer来设置词法分析器模式。该方法简单地取自stc模块中找到的STC_LEX_FOO值之一。

  2. 其次,它为词法分析器设置了关键字。关于每个词法分析器可用的关键字集的文档很少,因此有时需要查看 Scintilla 源代码以查看为每个词法分析器定义了哪些关键字集。Python 词法分析器支持两个关键字集:一个用于语言关键字,另一个用于用户定义的关键字。SetKeywords 方法接受两个参数:一个关键字 ID 和一个与该 ID 关联的空格分隔的关键字字符串。每个关键字 ID 都与给定词法分析器的 Style ID 相关联。在这个例子中,关键字 ID 为零的关联 Style ID 是:STC_P_WORD

  3. 第三点也是最后一点,它为所有的样式规范设置了 lexer。这与我们在基类中所做的方式相同,通过为 lexer 定义的每个样式规范 ID 调用StyleSetSpec来实现。有关哪些样式与哪些 lexer 相关联的快速参考可以在 wxPython wiki 中找到(wiki.wxpython.org/StyledTextCtrl%20Lexer%20Quick%20Reference)。

还有更多...

StyledTextCtrl 是一个包含非常庞大 API 的大类。它具有许多在这里未讨论的附加功能,例如用于实现自动完成的弹出列表、可点击的热点、代码折叠和自定义高亮显示。以下是一些关于 StyledTextCtrl 的参考资料和文档链接:

参见

  • 第三章中的使用 TextCtrl配方,用户界面基本构建块展示了如何使用基本文本控件。

  • 第十章中的 StyledTextCtrl 自定义高亮 菜单,创建组件和扩展功能 展示了如何扩展 StyledTextCtrl 以执行自定义文本样式。

使用托盘图标

桌面图标是集成到窗口管理器的任务栏(Windows/Linux)或 Dock(OS X)的 UI 组件。它们可用于通知,并在用户点击通知图标时提供弹出菜单。本食谱展示了如何通过使用TaskBarIcon类来创建和使用桌面图标。

如何做到这一点...

为了创建此菜谱示例代码的图标栏,我们创建了一个TaskBarIcon的子类,该子类加载一个用于显示的图像,并且当点击时具有显示Menu的处理功能:

class CustomTaskBarIcon(wx.TaskBarIcon):
    ID_HELLO = wx.NewId()
    ID_HELLO2 = wx.NewId()
    def __init__(self):
        super(CustomTaskBarIcon, self).__init__()

        # Setup
        icon = wx.Icon("face-monkey.png", wx.BITMAP_TYPE_PNG)
        self.SetIcon(icon)

        # Event Handlers
        self.Bind(wx.EVT_MENU, self.OnMenu)

    def CreatePopupMenu(self):
        """Base class virtual method for creating the
        popup menu for the icon.
        """
        menu = wx.Menu()
        menu.Append(CustomTaskBarIcon.ID_HELLO, "HELLO")
        menu.Append(CustomTaskBarIcon.ID_HELLO2, "Hi!")
        menu.AppendSeparator()
        menu.Append(wx.ID_CLOSE, "Exit")
        return menu

    def OnMenu(self, event):
        evt_id = event.GetId()
        if evt_id == CustomTaskBarIcon.ID_HELLO:
            wx.MessageBox("Hello World!", "HELLO")
        elif evt_id == CustomTaskBarIcon.ID_HELLO2:
            wx.MessageBox("Hi Again!", "Hi!")
        elif evt_id == wx.ID_CLOSE:
            self.Destroy()
        else:
            event.Skip()

它是如何工作的...

TaskBarIcon 类使用起来相当简单直接。需要做的只是创建一个图标,并调用 SetIcon 方法来创建将在系统托盘显示的对象的 UI 部分。然后我们重写 CreatePopupMenu 方法,这个方法是在图标被点击时基类会调用的,以创建菜单。这个方法需要做的只是创建一个 Menu 对象,然后返回它;TaskBarIcon 类会处理其余部分。最后,我们添加了一个 EVT_MENU 事件处理程序来处理来自我们的弹出菜单的菜单事件。

还有更多...

TaskBarIcon 类关联了许多事件,如果您想自定义不同类型点击的行为。请参阅以下表格以获取可用事件列表及其调用描述。

Events 描述
EVT_TASKBAR_CLICK 鼠标点击了图标
EVT_TASKBAR_LEFT_DCLICK 左键双击任务栏图标
EVT_TASKBAR_LEFT_DOWN 左键点击图标按下
EVT_TASKBAR_LEFT_UP 左键在图标上释放
EVT_TASKBAR_MOVE 任务栏图标已移动
EVT_TASKBAR_RIGHT_DCLICK 右键双击任务栏图标
EVT_TASKBAR_RIGHT_DOWN 右键鼠标点击图标按下
EVT_TASKBAR_RIGHT_UP 右键鼠标在图标上释放

参见

  • 第三章中的 添加菜单和菜单栏 菜谱,用户界面基本构建块 包含了更多使用和创建菜单的示例。

  • 第三章中的 如何使用弹出菜单 菜谱,用户界面基本构建块 包含了创建上下文菜单的另一个示例。

在笔记本中添加选项卡

Notebook类是一个容器控件,用于通过标签页管理多个面板。当选择一个标签页时,相关的面板会显示,而之前的一个面板会被隐藏。本食谱展示了如何使用默认的原生Notebook类创建一个类似于以下截图所示的基于标签页的用户界面:

将标签添加到笔记本

如何做到这一点...

以下示例代码片段定义了一个包含三个标签页的Notebook类:

class MyNotebook(wx.Notebook):
    def __init__(self, parent):
        super(MyNotebook, self).__init__(parent)

        # Attributes
        self.textctrl = wx.TextCtrl(self, value="edit me",
                                    style=wx.TE_MULTILINE)
        self.blue = wx.Panel(self)
        self.blue.SetBackgroundColour(wx.BLUE)
        self.fbrowser = wx.GenericDirCtrl(self)

        # Setup
        self.AddPage(self.textctrl, "Text Editor")
        self.AddPage(self.blue, "Blue Panel")
        self.AddPage(self.fbrowser, "File Browser")

它是如何工作的...

这个示例仅仅展示了如何使用Notebook控件的基本原理。我们简单地创建了一些我们希望放入Notebook中的窗口对象。在这种情况下,我们创建了三个不同的对象:一个TextCtrl,一个Panel和一个GenericDirCtrl。需要注意的是,我们希望放入Notebook中的项目必须是Notebook的子对象。

然后将对象通过调用其AddPage方法添加到Notebook中。此方法接受一个窗口对象和一个标签作为参数。

还有更多...

基本的Notebook类并没有提供太多超出上述展示的功能。然而,还有一些额外的样式,以及一些与选项卡选择相关的事件。以下是一些关于这些额外项目的快速参考。

样式

Notebook 构造函数可以提供以下样式:

样式 描述
NB_BOTTOM 将选项卡放置在控制区域的底部
NB_FIXEDWIDTH 所有制表符大小相同(仅限 Windows)
NB_LEFT 将选项卡放置在控制区域的左侧
NB_MULTILINE 允许多行制表符(仅限 Windows)
NB_NOPAGETHEME 仅在 Windows 系统中使用纯色作为标签页颜色
NB_RIGHT 将选项卡放置在控制区域的右侧
NB_TOP 将选项卡置于控制区域顶部(默认)

事件

笔记本会发出以下事件:

事件 描述
EVT_NOTEBOOK_PAGE_CHANGING 当笔记本正在从一个页面切换到另一个页面时,会触发此事件。在事件对象上调用 Veto 将阻止页面切换。
EVT_NOTEBOOK_PAGE_CHANGED 当选中的页面发生变化时,将触发此事件。

参见

  • 本章中关于使用 FlatNotebook 食谱的部分展示了另一种类型标签控制的用法。

使用 FlatNotebook

FlatNotebook 类是一个自定义的 Notebook 实现,它提供了比默认 Notebook 更多的功能。这些额外功能包括在每个标签页上拥有关闭按钮、拖放标签页到不同的位置,以及多种不同的标签页样式来改变控件的外观和感觉。本食谱将探索这个控件提供的部分扩展功能。

如何做到这一点...

作为如何使用 FlatNotebook 的一个示例,我们将定义一个具有显示多个 TextCtrls: 特殊化的子类:

import wx
import wx.lib
import wx.lib.flatnotebook as FNB

class MyFlatNotebook(FNB.FlatNotebook):
    def __init__(self, parent):
        mystyle = FNB.FNB_DROPDOWN_TABS_LIST|\
                  FNB.FNB_FF2|\
                  FNB.FNB_SMART_TABS|\
                  FNB.FNB_X_ON_TAB
        super(MyFlatNotebook, self).__init__(parent,
                                             style=mystyle)
        # Attributes
        self._imglst = wx.ImageList(16, 16)

        # Setup
        bmp = wx.Bitmap("text-x-generic.png")
        self._imglst.Add(bmp)
        bmp = wx.Bitmap("text-html.png")
        self._imglst.Add(bmp)
        self.SetImageList(self._imglst)

        # Event Handlers
        self.Bind(FNB.EVT_FLATNOTEBOOK_PAGE_CLOSING, 
                  self.OnClosing)

    def OnClosing(self, event):
        """Called when a tab is closing"""
        page = self.GetCurrentPage()
        if page and hasattr(page, "IsModified"):
            if page.IsModified():
                r = wx.MessageBox("Warning unsaved changes”
                                  “ will be lost",
                                  "Close Warning",
                                  wx.ICON_WARNING|\
                                  wx.OK|wx.CANCEL)
                if r == wx.CANCEL:
                    event.Veto()

在下面的屏幕截图里,我们可以看到上述子类在实际操作中的表现,在一个简单的文件编辑器应用中。下面应用的完整源代码以及与这个食谱一起提供的代码都可以获取。

如何做...

它是如何工作的...

这份小食谱展示了FlatNotebook相对于标准Notebook类提供的许多特性。因此,让我们逐节分析,从构造函数开始。

在我们子类的构造函数中,我们指定了四个样式标志。第一个,FNB_DROPDOWN_TAB_LIST,指定了我们想要一个下拉列表,显示所有打开的标签页。下拉列表是那个小向下箭头按钮:点击它将显示一个弹出菜单,允许从列表中选择当前打开的标签页之一。第二个样式标志,FNB_FF2,指定了我们想要使用 Firefox 2 标签渲染器的标签页,这将绘制出看起来和感觉类似于 Firefox 2 中的标签页。第三个样式标志,FNB_SMART_TABS,指定了Ctrl + Tab快捷键将弹出一个对话框,显示打开的标签页,并允许通过按Tab键在它们之间循环。我们使用的第四个和最后一个样式标志,FNB_X_ON_TAB,指定了我们想要在活动标签页上显示一个关闭按钮。这允许用户在点击此按钮时关闭标签页。

为了能够在标签上显示图标,我们还为该控件创建并分配了一个ImageListImageList简单来说是一个用于存放Bitmap对象的容器,控件在绘制标签时会使用它来检索位图数据。需要注意的是,我们通过将其分配给self._imglst来保留对该对象的引用;保留引用是很重要的,这样它就不会被垃圾回收。

我们最后做的是将控制与页面关闭事件EVT_FLATNOTEBOOK_PAGE_CLOSING绑定。在这个例子中,我们期望我们的页面提供一个IsModified方法,这样我们就可以在关闭页面之前检查是否有未保存的更改,以便给用户一个取消关闭页面的机会。

还有更多...

因为FlatNotebook是一个纯 Python 类,所以它比基本的Notebook类更可定制。以下是可以用来定制控件外观和行为的样式标志列表:

样式标志

这里是其他可用的样式标志列表,我们之前还没有介绍过:

样式标志 描述
FNB_ALLOW_FOREIGN_DND 允许标签被拖动并移动到其他 FlatNotebook 实例,并从其他实例接受标签
FNB_BACKGROUND_GRADIENT 在标签页背景区域绘制渐变
FNB_BOTTOM 将选项卡放置在控制区域的底部
FNB_COLORFUL_TABS 使用彩色标签(仅 VC8 样式)
FNB_DCLICK_CLOSES_TABS 允许双击关闭活动标签
FNB_DEFAULT_STYLE FNB_MOUSE_MIDDLE_CLOSES_TABSFNB_HIDE_ON_SINGLE_TAB 的组合
FNB_FANCY_TABS 使用 fancy 标签渲染器来绘制标签
FNB_HIDE_ON_SINGLE_TAB 当只有一个标签打开时隐藏标签容器区域
FNB_MOUSE_MIDDLE_CLOSES_TABS 允许鼠标中键点击关闭标签
FNB_NODRAG 不允许标签拖放
FNB_NO_NAV_BUTTONS 不显示标签滚动按钮
FNB_NO_X_BUTTON 不要在标签容器区域的右侧显示X按钮
FNB_TABS_BORDER_SIMPLE 在页面周围绘制细边框
FNB_VC71 使用 Visual Studio 2003 风格的制表符
FNB_VC8 使用 Visual Studio 2005 风格的选项卡

参见

  • 本章中关于添加标签页到笔记本的配方展示了如何使用基本的标签控制功能。

使用 ScrolledPanel 进行滚动

ScrolledPanel 类是一个内置 ScrollBars 的自定义 Panel 类。这个类由 wx.lib 模块中的 scrolledpanel 提供。默认情况下,Panels 在内容超出给定窗口区域时没有滚动能力。这个菜谱展示了如何使用 ScrolledPanel,通过它来创建一个自定义的图像列表小部件。

如何做到这一点...

要创建我们自定义的图像查看器控件,该控件使用ScrolledPanel,我们将定义这样一个简单的类来管理一系列Bitmaps

import wx
import wx.lib.scrolledpanel as scrolledpanel

class ImageListCtrl(scrolledpanel.ScrolledPanel):
    """Simple control to display a list of images"""
    def __init__(self, parent, bitmaps=list(),
                 style=wx.TAB_TRAVERSAL|wx.BORDER_SUNKEN):
        super(ImageListCtrl, self).__init__(parent,
                                            style=style)

        # Attributes
        self.images = list()
        self.sizer = wx.BoxSizer(wx.VERTICAL)

        # Setup
        for bmp in bitmaps:
            self.AppendBitmap(bmp)
        self.SetSizer(self.sizer)

    def AppendBitmap(self, bmp):
        """Add another bitmap to the control"""
        self.images.append(bmp)
        sbmp = wx.StaticBitmap(self, bitmap=bmp)
        self.sizer.Add(sbmp, 0, wx.EXPAND|wx.TOP, 5)
        self.SetupScrolling()

它是如何工作的...

ScrolledPanel 使得与 ScrollBars 一起工作变得非常简单,所以让我们快速了解一下它是如何工作的。

我们创建了一个简单的类,名为 ImageListCtrl。这个控件可以用来显示位图列表。我们从这个类派生出了 ScrolledPanel,这样如果包含很多图片,用户就可以滚动查看所有图片。使用 ScrolledPanel 的唯一特殊之处在于,当所有面板的子控件都添加到其 Sizer 中时,需要调用它的 SetupScrolling 方法。通常,这会在子类的 __init__ 方法中完成,但由于我们的小部件可以在任何时候添加更多的 Bitmap 项目,因此我们需要在 AppendBitmap 方法中添加每个 Bitmap 之后调用它。

SetupScrolling 方法通过计算 Panel 内容的最小尺寸,并为 ScrollBar 对象设置与之协同工作的虚拟区域大小来工作。

简化 FoldPanelBar

FoldPanelBar 是一个自定义容器类,它允许将多个控件组合成 FoldPanelItem 控件,通过点击其 CaptionBar 来展开或收缩。FoldPanelBar 不与基于 Sizer 的布局一起工作,因此其 API 可能会有些繁琐,因为它需要你逐个添加每个控件并使用各种标志来设置其布局。本菜谱展示了如何创建一个与 Panel 对象一起工作的自定义 FoldPanelBar。这个类将允许你将代码模块化到 Panel 类中,然后只需将它们添加到 FoldPanelBar 而不是直接将所有内容添加到 FoldPanelBar 本身。

如何做到这一点...

这个自定义的FoldPanelBar类采用工厂方法来简化并抽象化向控件添加新的Panel的过程:

import wx
import wx.lib.foldpanelbar as foldpanel

class FoldPanelMgr(foldpanel.FoldPanelBar):
    """Fold panel that manages a collection of Panels"""
    def __init__(self, parent, *args, **kwargs):
        super(FoldPanelMgr, self).__init__(parent,
                                           *args,
                                           **kwargs)

    def AddPanel(self, pclass, title=u"", collapsed=False):
        """Add a panel to the manager
        @param pclass: Class constructor (callable)
        @keyword title: foldpanel title
        @keyword collapsed: start with it collapsed
        @return: pclass instance
        """
        fpitem = self.AddFoldPanel(title, collapsed=collapsed)
        wnd = pclass(fpitem)
        best = wnd.GetBestSize()
        wnd.SetSize(best)
        self.AddFoldPanelWindow(fpitem, wnd)
        return wnd

它是如何工作的...

我们对 FoldPanelBar 的子类添加了一个新的方法到 AddPanel 类。AddPanel 方法是对 FoldPanelBar 控件的 AddFoldPanelAddFoldPanelWindow 方法的简单封装。AddFoldPanel 方法用于创建控制件的 CaptionBar 和容器,而 AddFoldPanelWindow 方法用于将一个窗口对象添加到 FoldPanel

我们的 AddPanel 方法将其第一个参数接受为一个可调用对象。这个可调用对象必须接受一个 "parent" 参数,并在被调用时返回一个新窗口,该窗口是父窗口的子窗口。我们这样做是因为我们的面板需要作为 AddFoldPanel 返回的 FoldPanelItem 的子窗口来创建。这是在使用 FoldPanelBar 时需要记住的一个重要点。添加到其中的所有控件都必须是其中一个 FoldPanelItems 的子控件,而不是 FoldPanelBar 本身的子控件。

由于FoldPanelBar内部使用手动布局,因此在我们添加每个Panel时,需要为每个Panel设置一个显式的大小。这是通过获取每个Panel对象的GetBestSize方法的最佳大小来完成的。

还有更多...

FoldPanelBarCaptionBar 可以通过创建一个自定义的 CaptionBarStyle 对象并将其传递给 AddFoldPanel 方法来自定义。CaptionBarStyle 对象具有用于更改 CaptionBar 将使用的颜色、字体和样式的各种方法。AddFoldPanel 方法还接受一个可选的 foldIcons 参数,该参数接受一个必须包含两个 16x16 像素位图的 ImageList 对象。第一个将被用于按钮的展开状态,第二个将被用于其折叠状态。

第五章:提供信息和提醒用户

在本章中,我们将涵盖:

  • 显示一个 消息框

  • 提供对 ToolTips 的帮助

  • 使用超级工具提示

  • 显示一个气球提示

  • 创建自定义的 SplashScreen

  • 使用进度对话框显示任务进度

  • 创建一个 关于框

简介

在应用程序运行期间,在多种不同情况下可能会出现各种各样的事件。这导致了需要能够以同样广泛的各种上下文敏感、直观和有效的方式提醒和通知用户这些事件的需求。

在正确的时间和方式提供信息对于应用程序的可用性至关重要。wxPython 包含许多小部件来帮助满足任何类型应用程序的特定需求。因此,让我们来看看这些小部件,并了解如何充分利用它们。

显示消息框

消息框是(如果不是)最常见和最简单的方式之一,用于提醒用户并为他们提供做出简单选择的能力。消息框有多种不同的形式,但都共享两个共同点。它们都有一个(通常是)简短的标题消息和一个或多个按钮,允许用户对消息做出回应。这个配方展示了如何添加一个消息框,让用户有机会取消关闭框架

如何做到这一点...

作为一个如何显示MessageBox的例子,我们将创建一个小的Frame类,它使用MessageBox作为对窗口关闭事件的确认:

class MyFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(MyFrame, self).__init__(parent, *args, **kwargs)

        # Layout
        self.CreateStatusBar()
        self.PushStatusText("Close this window")

        # Event Handlers
        self.Bind(wx.EVT_CLOSE, 

    def OnClose(self, event):
        result = wx.MessageBox("Are you sure you want "
                               "to close this window?",
                               style=wx.CENTER|\
                                     wx.ICON_QUESTION|\
                                     wx.YES_NO)
        if result == wx.NO:
            event.Veto()
        else:
            event.Skip()

如何做...

它是如何工作的...

wx.MessageBox 是一个创建、显示和清理模态对话框的函数。它只需要第一个参数,该参数指定了将要显示的消息:

wx.MessageBox(message, caption=””, style=wx.OK|wx.CENTER,
                               parent=None, x=-1, y=-1)

其他参数都是可选的关键字参数。第二个参数用于指定对话框的标题。第三个参数是样式参数,用于指定对话框的外观以及它将包含哪些按钮。这个参数就像任何其他小部件构造函数一样,其值将是一个样式标志的位掩码。第四个参数可以用来指定对话框的父窗口。最后两个参数可以用来在桌面上显式设置对话框的 X 和 Y 坐标。

在这个示例中,我们只使用了消息和样式参数。在样式参数中,我们指定了CENTER标志,表示对话框应在其父元素上居中显示,在这种情况下,由于我们没有指定父窗口,所以将是桌面。ICON_QUESTION标志指定我们希望在对话框上显示问号图标。最后一个标志YES_NO表示我们希望在对话框上有一个是/否按钮,以便用户可以回复我们消息中提出的是/否问题。

当用户点击对话框中的一个按钮时,对话框将结束其模态循环并返回被点击按钮的值,在这种情况下将是YESNO。在这里,我们简单地检查返回值,要么否决事件以阻止Frame关闭,要么跳过它以允许Frame被销毁。

注意

平台公告

在 OS X 上,这些对话框将显示应用程序图标。这样做是为了符合苹果的人机界面指南。这意味着除非你已经将你的脚本构建成一个小程序并为其指定了图标,否则对话框将显示 Python.app 图标。

还有更多...

MessageBox 函数可以接受多个样式标志。以下是一个按类别划分的快速参考列表。

图标

MessageBox只能显示一个图标,因此一次只能指定以下标志中的一个:

标志 描述
wx.ICON_ERROR 在对话框上显示一个图标,表示已发生错误。
wx.ICON_INFORMATION 在对话框上显示一个图标,表示该对话框仅显示信息。
wx.ICON_QUESTION 在对话框上显示一个图标,表示用户需要回答的问题。
wx.ICON_WARNING 在对话框上显示一个图标,表示向用户显示警告信息。

按钮

以下标志用于指定在对话框中显示的按钮。默认情况下,对话框将仅显示一个“确定”按钮:

Flags 描述
wx.CANCEL 向对话框添加一个取消按钮。
wx.OK 在对话框中添加一个确定按钮。
wx.YES 添加一个“是”按钮到对话框。
wx.NO 在对话框中添加一个“否”按钮。
wx.YES_NO wx.YES/wx.NO的便捷性。
wx.YES_DEFAULT 设置“是”按钮为默认按钮。
wx.NO_DEFAULT 设置“否”按钮为默认按钮。

提供工具提示的帮助

工具提示ToolTips)是当鼠标光标在窗口对象上停留片刻时显示的小型弹出帮助文本。当鼠标离开窗口区域时,它们会自动消失。在需要向用户展示关于界面某部分功能额外信息的场合,它们非常有用。几乎所有的窗口对象都支持与它们关联一个工具提示。本食谱展示了如何向一个按钮添加一个工具提示

如何做到这一点...

为了了解如何向控件添加ToolTip,让我们先创建一个简单的Panel类,它上面有一个单独的Button

class ToolTipTestPanel(wx.Panel):
    def __init__(self, parent):
        super(ToolTipTestPanel, self).__init__(parent)

        # Attributes
        self.button = wx.Button(self, label="Go")

        # Setup
        self.button.SetToolTipString("Launch the shuttle")
        self.timer = wx.Timer(self)
        self.count = 11

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.button, 0, wx.ALIGN_CENTER)
        msizer = wx.BoxSizer(wx.HORIZONTAL)
        msizer.Add(sizer, 1, wx.ALIGN_CENTER)
        self.SetSizer(msizer)

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnGo, self.button)
        self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)

    def OnGo(self, event):
        self.button.Disable()
        print self.timer.Start(1000)
        tlw = self.GetTopLevelParent()
        tlw.PushStatusText("Launch initiated...")

    def OnTimer(self, event):
        tlw = self.GetTopLevelParent()
        self.count -= 1
        tlw.PushStatusText("%d" % self.count)
        if self.count == 0:
            self.timer.Stop()
            wx.MessageBox("Shuttle Launched!")

它是如何工作的...

在这里,我们仅仅创建了一个带有单个按钮的简单面板。按钮上只有一个简单的标签,写着Go。由于没有其他指示说明这个按钮可能执行的操作,我们随后通过调用SetToolTipString方法向其添加了一个ToolTipSetToolTipString方法属于基类wx.Window,因此它可以与屏幕上任何可见的对象一起使用。此方法创建一个ToolTip对象,然后调用窗口的SetToolTip方法将ToolTipWindow关联起来。

还有更多...

  • 本章中的 使用 SuperToolTips 菜单展示了为用户提供上下文相关帮助的另一种方法。

  • 请参阅第二章中的鼠标玩转技巧食谱,了解系统如何在鼠标光标进入窗口时显示提示的细节。

使用 SuperToolTips

SuperToolTip 类是由 wx.lib.agw.supertooltip 模块提供的高级 ToolTip 类型。与常规的 ToolTip 不同,SuperToolTip 是一个自定义的全拥有者绘制的控件,支持广泛的显示选项。它具有显示标题、页脚、主体部分的能力,并且每个部分也可以在其中显示图片。除此之外,它还支持自定义背景、内容的 HTML 渲染和超链接。本食谱展示了如何创建和使用 SuperToolTip

注意事项

wx.lib.agw 在 wxPython 2.8.9.2 及更高版本中可用。

如何做到这一点...

让我们修改来自“使用 ToolTips 提供帮助”菜谱的示例,以展示更详细的帮助信息在这种情况下可能是有益的。这个菜谱使用了两个与本章源代码一起提供的图像文件:

import wx.lib.agw.supertooltip as supertooltip

class SuperToolTipTestPanel (wx.Panel):
    def __init__(self, parent):
        super(SuperToolTipTestPanel, self).__init__(parent)

        # Attributes
        self.button = wx.Button(self, label="Go")
        msg = "Launches the shuttle"
        self.stip = supertooltip.SuperToolTip(msg)

        # Setup SuperToolTip
        bodybmp = wx.Bitmap("earth.png", wx.BITMAP_TYPE_PNG)
        self.stip.SetBodyImage(bodybmp)
        self.stip.SetHeader("Launch Control")
        footbmp = wx.Bitmap("warning.png", wx.BITMAP_TYPE_PNG)
        self.stip.SetFooterBitmap(footbmp)
        footer = "Warning: This is serious business"
        self.stip.SetFooter(footer)          
        self.stip.ApplyStyle("XP Blue")
        self.stip.SetTarget(self.button)

如何做...

它是如何工作的...

在这里,我们修改了之前的配方,将其改为使用SuperToolTip而不是标准的ToolTip

首先,我们从wx.lib.agw包中导入扩展模块,以便我们可以访问SuperToolTip类。然后,我们在SuperToolTipTestPanel类中为我们的按钮创建SuperToolTip对象。在我们的用例中,这包括创建带有其正文消息的提示,然后设置一个将显示在我们消息左侧的正文图像。我们随后通过调用SetHeader方法添加了一些标题文本,以及通过使用SetFooterSetFooterBitmap方法添加了页脚和页脚图像。最后一步设置是调用ApplyStyle方法。此方法允许使用大约 30 种内置样式主题之一来设置背景渐变。

使用 SuperToolTip 的最后一步是它与标准 ToolTip 之间的重要区别。我们必须在 SuperToolTip 对象上调用 SetTarget 而不是在 Window 对象(在这种情况下是我们的 Button)上调用 SetToolTip。这是因为 SuperToolTip 管理其显示/隐藏的时间,而不是它所属的 Window

还有更多...

SuperToolTip 包含一些本食谱未涵盖的选项。以下是一些额外的参考和信息。

样式

ApplyStyles 方法接受一个命名内置样式作为参数。目前大约有 30 种不同的内置样式。它们都在 supertooltip 模块中定义,并且可以通过查看 supertooltip.GetStyleKeys() 的返回值来找到,该函数将返回所有内置样式的列表。

额外的自定义

有几种额外的方法可以用来自定义SuperToolTip的外观。以下表格包含了这些方法的快速参考:

方法 描述
SetDrawHeaderLine(bool) 在标题和正文之间绘制一条水平分隔线。
SetDrawFooterLine(bool) 在页脚和主体之间绘制一条水平分隔线。
SetDropShadow(bool) 在提示窗口上使用阴影效果。(仅限 Windows)
SetUseFade(bool) 在视图中淡入/淡出。(仅限 Windows)
SetEndDelay(int) 设置提示显示的时间。
SetTopGradientColour(colour) 设置顶部渐变颜色。
SetBottomGradientColour(colour) 设置底部渐变颜色。
SetMiddleGradientColour(colour) 设置中间渐变颜色。

参见

  • 本章中的“使用 ToolTips 提供帮助”配方展示了提供简单帮助信息的方法。

显示气球提示

BalloonTips 是另一种 ToolTip 实现。它们与 SuperToolTip 相当相似,但外观和感觉模仿了 Windows XP 任务栏的气球通知。当显示时,BalloonTip 将创建一个具有指向其目标窗口中心的尖点的提示窗口。本食谱展示了如何将 BalloonTip 添加到应用程序的 TaskBarIcon

注意

wx.lib.agw 在 wxPython 2.8.9.2 及更高版本中可用。

如何做到这一点...

在这里,我们将创建一个简单的 TaskBarIcon 类,当鼠标悬停在其上时将显示一个 BalloonTip。在这个示例中,我们再次使用与本章源代码一起提供的外部图标:

import wx.lib.agw.balloontip as btip

class TaskBarBalloon(wx.TaskBarIcon):
    def __init__(self):
        super(TaskBarBalloon, self).__init__()

        # Setup
        icon = wx.Icon("face-monkey.png", wx.BITMAP_TYPE_PNG)
        self.SetIcon(icon)

        # Setup BallooTip
        title="BalloonTip Recipe"
        msg = "Welcome to the Balloon Tip Recipe"
        bmp = wx.BitmapFromIcon(icon)
        self.tip = btip.BalloonTip(topicon=bmp,
                                   toptitle=title,
                                   message=msg,
                                   shape=btip.BT_ROUNDED,
                                   tipstyle=btip.BT_BUTTON)
        self.tip.SetStartDelay(1000)
        self.tip.SetTarget(self)

        # Event Handlers
        self.Bind(wx.EVT_MENU, self.OnMenu)

    def CreatePopupMenu(self):
        menu = wx.Menu()
        menu.Append(wx.ID_CLOSE, "Exit")
        return menu

    def OnMenu(self, event):
        self.RemoveIcon()
        self.tip.DestroyTimer()
        self.Destroy()

它是如何工作的...

BalloonTip 类位于 wx.lib.agw.balloontip 模块中。为了便于使用,我们使用别名 btip 导入了它。BalloonTip 构造函数最多接受五个可选的关键字参数,用于指定提示框的内容和外观:

关键字参数 描述
topicon 接受一个将在 BalloonTip 窗口的左上角显示的 Bitmap 对象
toptitle 一个指定 BalloonTip 窗口标题的字符串
message 一个指定 BalloonTip 窗口主要信息的字符串
形状 要么是 BT_RECTANGLEBT_ROUNDED(默认),用于指定 BalloonTip 窗口的形状

| tipstyle | 指定如何关闭 BalloonTip 窗口的以下值之一:

  • BT_LEAVE: 当鼠标离开目标窗口时,提示窗口将被关闭

  • BT_CLICK: 当用户点击目标窗口时,提示窗口将被关闭

  • BT_BUTTON: 点击提示窗口上的关闭按钮将关闭提示窗口

在创建了BalloonTip之后,我们通过调用SetStartDelay方法来修改其启动延迟,该方法设置从鼠标移动到目标窗口后显示提示窗口的延迟时间(以毫秒为单位)。最后,我们调用SetTarget来将TaskBarIcon设置为该BalloonTip的目标窗口。之后,BalloonTip就全部设置好了,可以与我们的TaskBarIcon一起使用。

在退出菜单事件的TaskBarIcons事件处理器中,我们不得不在我们的BalloonTip上添加对DestroyTimer的调用。这是为了确保提示窗口被销毁,否则如果它仍然打开,由于应用程序中仍有顶层窗口,应用程序的主循环将不会退出。

还有更多...

wx.lib中大多数通用小部件一样,BalloonTip类提供了多种方法来自定义其外观。下表包含了对这些方法的一些快速参考:

方法 描述
SetBalloonColour(colour) 设置 BalloonTips 的背景颜色。传递 None 以恢复到默认值。
SetMessageColour(colour) 设置主消息的文本 颜色。传递 None 以恢复到默认设置。
SetMessageFont(font) 设置用于主要信息的 Font。传递 None 以恢复到默认设置。
SetTitleColour(colour) 设置标题的文本 颜色。传递 None 以恢复到默认设置。
SetTitleFont(font) 设置标题 字体. 通过传递 None 来恢复默认设置。

参见

  • 本章中的使用 SuperToolTips配方展示了提供上下文相关信息消息的另一种方法。

  • 第四章中的使用托盘图标配方,用户界面的高级构建块包含了在应用程序中使用TaskBarIcons的解释。

创建自定义启动画面

SplashWindows 在应用程序启动过程中很常见。它们是用来显示软件标志的一种方式,更重要的是,它们通常被用作在应用程序启动需要一些时间时向用户提供反馈的手段,这样用户就会知道应用程序正在加载过程中。这个配方展示了如何创建一个高级的 SplashWindow 类,它可以显示应用程序在启动过程中的增量进度。

如何做到这一点...

在这里,我们将创建我们的自定义SplashScreen类。本章附带的源代码还包括一个示例应用程序,展示如何使用这个类:

class ProgressSplashScreen(wx.SplashScreen):
    def __init__(self, *args, **kwargs):
        super(ProgressSplashScreen, self).__init__(*args,
                                                   **kwargs)

        # Attributes
        self.gauge = wx.Gauge(self, size=(-1, 16))

        # Setup
        rect = self.GetClientRect()
        new_size = (rect.width, 16)
        self.gauge.SetSize(new_size)
        self.SetSize((rect.width, rect.height + 16))
        self.gauge.SetPosition((0, rect.height))

    def SetProgress(self, percent):
        """Set the indicator gauges progress"""
        self.gauge.SetValue(percent)

    def GetProgress(self):
        """Get the current progress of the gauge"""
        return self.gauge.GetValue()

截图如下:

如何做...

它是如何工作的...

基本的 SplashScreen 使用传递给构造函数的 Bitmap 来设置其大小,然后 Bitmap 被绘制以填充背景。在我们的子类中,我们创建了一个 Gauge 以允许程序向用户反馈启动过程的进度。

为了让Gauge适应SplashScreen,我们首先通过调用其SetSize方法,并传入Gauge的高度和SplashScreen的宽度,将Gauge的宽度改为与SplashScreen相同。接下来,我们将SplashScreen的大小调整为更高,以便我们可以将Gauge放置在其底部而不与SplashScreen图像重叠。然后,最后一步是通过调用其SetPosition方法,并传入Gauge顶部左角应放置的 X 和 Y 坐标,手动将Gauge定位到我们在底部添加的额外空间中。

我们为这个类添加的最后两件事只是允许用户通过这个类来操作Gauge的一些简单访问方法。为了了解这个类在实际中的应用,请查看本章附带的示例代码。

还有更多...

SplashScreen 构造函数有两个不同的样式参数。第一个参数,splashStyle,是一个必需的位掩码,可以是以下一个或多个标志的组合:

标志 描述
wx.SPLASH_CENTRE_ON_PARENT 将启动画面居中显示在其父窗口上。
wx.SPLASH_CENTRE_ON_SCREEN 将启动画面居中显示在桌面上。
wx.SPLASH_NO_CENTRE 不要居中显示启动画面。
wx.SPLASH_TIMEOUT 允许当超时达到时自动销毁启动画面。
wx.SPLASH_NO_TIMEOUT 不允许启动画面超时(需要显式销毁它)。

第二个样式标志参数是典型的可选参数,用于指定 wx.Frame 样式标志。SplashScreenwx.Frame 派生,因此这些标志将传递到基类。在大多数情况下,默认标志就是这里想要使用的,否则它最终的行为会更像 Frame 而不是 SplashScreen

使用进度对话框显示任务进度

ProgressDialog 是一个用于显示长时间运行任务进度的对话框,例如从互联网下载文件或从您的程序中导出数据。该对话框显示一条简短的消息、一个进度条,以及可选的“中止”和/或“跳过”按钮。此外,它还可以可选地显示估计时间、已用时间和剩余时间。本食谱展示了如何制作一个命令行脚本,该脚本可用于从互联网下载文件,并使用 ProgressDialog 显示下载进度。

如何做到这一点...

我们将在这里创建一个完整的应用程序,以便从命令行传递给脚本的 URL 下载文件。因此,首先我们将定义应用程序对象类:

import wx
import os
import sys
import urllib2

class DownloaderApp(wx.App):
    def OnInit(self):
        # Create a hidden frame so that the eventloop
        # does not automatically exit before we show
        # the download dialog.
        self.frame = wx.Frame(None)
        self.frame.Hide()
        return True

    def Cleanup(self):
        self.frame.Destroy()

在这里,我们定义了将要用来显示ProgressDialog的方法,以及通过使用 Python 标准库中的urllib模块来实际下载文件的方法:

    def DownloadFile(self, url):
        """Downloads the file
        @return: bool (success/fail)
        """
        dialog = None
        try:
            # Open the url to read from and
            # the local file to write the downloaded
            # data to.
            webfile = urllib2.urlopen(url)
            size = int(webfile.info()['Content-Length'])
            dlpath = os.path.abspath(os.getcwd())
            dlfile = url.split('/')[-1]
            dlpath = GetUniqueName(dlpath, dlfile)
            localfile = open(dlpath, 'wb')

            # Create the ProgressDialog
            dlmsg = "Downloading: %s" % dlfile
            style = (wx.PD_APP_MODAL
                     |wx.PD_CAN_ABORT
                     |wx.PD_ELAPSED_TIME
                     |wx.PD_REMAINING_TIME)
            dialog = wx.ProgressDialog("Download Dialog",
                                       dlmsg,
                                       maximum=size,
                                       parent=self.frame,
                                       style=style)

            # Download the file
            blk_sz = 4096
            read = 0
            keep_going = True
            while read < size and keep_going:
                data = webfile.read(blk_sz)
                localfile.write(data)
                read += len(data)
                keep_going, skip = dialog.Update(read)

            localfile.close()
            webfile.close()
        finally:
            # All done so cleanup top level windows
            # to cause the event loop to exit.
            if dialog:
                dialog.Destroy()
            self.Cleanup()

在这里,我们有一个额外的辅助函数,用于获取一个唯一的路径,以便将下载的数据写入:

#--- Utility Functions ----#

def GetUniqueName(path, name):
    """Make a file name that will be unique in case a file
    of the same name already exists at that path.
    @param path: Root path to folder of files destination
    @param name: desired file name base
    @return: string
    """
    tmpname = os.path.join(path, name)
    if os.path.exists(tmpname):
        if '.' not in name:
            ext = ''
            fbase = name
        else:
            ext = '.' + name.split('.')[-1]
            fbase = name[:-1 * len(ext)]

        inc = len([x for x in os.listdir(path)
                   if x.startswith(fbase)])
        newname = "%s-%d%s" % (fbase, inc, ext)
        tmpname = os.path.join(path, newname)
        while os.path.exists(tmpname):
            inc = inc + 1
            newname = "%s-%d%s" % (fbase, inc, ext)
            tmpname = os.path.join(path, newname)

    return tmpname

最后,执行该脚本的主体部分如下,它为该应用程序处理简单的命令行参数并开始下载:

#---- Main Execution ----#
if __name__ == "__main__":
    if len(sys.argv) > 1:
        url = sys.argv[1]
        app = DownloaderApp(False)
        # Start with a slight delay so the eventloop
        # can start running, to ensure our dialog gets
        # shown
        wx.CallLater(2000, app.DownloadFile, url)
        app.MainLoop()
    else:
        # Print some help text
        print(("wxPython Cookbook - ProgressDialog\n"
               "usage: downloader url\n"))

这里是一个从命令行下载器调用此脚本的示例:python downloader.py http://somewebsite.com/afile.zip.

它是如何工作的...

上面的食谱展示了整个下载应用程序的代码。让我们从顶部开始,一步步了解它是如何工作的。

首先,我们从标准 Python 库中导入了某些模块。我们需要 os 模块来进行路径操作,sys 模块来获取命令行参数,以及 urllib2 模块以便我们可以打开远程 URL。

接下来,我们定义了我们的 DownloaderApp。这个应用程序对象有两个对我们来说很有兴趣的方法。第一个是重写 wx.App.OnInit。在我们的重写中,我们创建了一个框架并将其隐藏。我们这样做只是为了确保在创建和显示我们的 ProgressDialog 之前,事件循环不会退出,因为当应用程序中没有更多顶层窗口时,事件循环将默认退出。第二个方法是 DownloadFile。这是这个应用程序主要动作发生的地方。

DownloadFile 首先使用 urllib2 打开传入的远程 URL,并获取该 URL 指向的文件大小。接下来,它在本地文件系统中打开一个文件,以便我们在从远程 URL 读取数据时写入。然后,我们创建我们的 ProgressDialog,给它必要的样式标志以包含一个中止按钮并显示已过时间和剩余时间。一旦对话框创建完成,我们就可以开始从我们打开的 URL 读取数据。我们通过一个循环来完成这个操作,该循环检查我们已经读取了多少数据以及中止按钮是否被点击。从 URL 读取一块数据后,我们调用 ProgressDialogUpdate 方法,该方法将更新进度条并返回两个布尔标志,指示是否点击了两个可能的对话框按钮之一。一旦循环退出,我们只需关闭两个文件并 Destroy 我们窗口对象,以使主循环退出。

最后两件事是GetUniqueName函数和__main____main__只是一个简单的实用函数,用来帮助生成本地文件名,以确保我们不会尝试覆盖已经存在的文件。__main__执行部分只是对命令行参数进行简单的检查,然后创建DownloaderApp实例并调用其DownloadFile方法。我们需要使用wx.CallLater来延迟调用几秒钟,因为当调用DownloadFile时,它将会阻塞。如果不使用CallLater,它将会阻塞,执行下载,并在MainLoop开始之前返回,这意味着我们的对话框永远不会在屏幕上显示。

还有更多...

以下包含了一些在使用ProgressDialog时需要考虑的额外参考信息和资料。

消息参数

在某些情况下,使用ProgressDialog的消息参数可能会出现一些不理想的行为。如果传入的消息字符串非常长,它会导致对话框的宽度被设置得非常宽。所以如果你看到对话框显示的宽度比你预期的要宽得多,尝试缩短你的消息。

样式标志

这里是ProgressDialog可用的样式标志的快速参考:

样式标志 描述
wx.PD_APP_MODAL 对话框应该是应用程序模式
wx.PD_AUTO_HIDE 当进度条达到最大值时,对话框将自动消失
wx.PD_SMOOTH 使进度条平滑更新
wx.PD_CAN_ABORT 在对话框上显示“取消”按钮
wx.PD_CAN_SKIP 在对话框上显示跳过按钮
wx.PD_ELAPSED_TIME 显示已过时间状态文本
wx.PD_ESTIMATED_TIME 显示预估的总时间状态文本
wx.PD_REMAININT_TIME 显示预估剩余时间状态文本

创建一个关于框(AboutBox)

关于对话框是一个简单的对话框,用于向用户显示有关应用程序的一些信息,例如应用程序的版本号和许可信息。这个对话框可以在大多数操作系统上的任何应用程序中找到。它包含一个图标和一个小信息区域,通常至少包含版本信息和致谢。本食谱展示了如何在应用程序中设置和显示关于对话框。

如何做到这一点...

在这里,我们创建了一个简单的骨架应用程序,展示了如何将AboutBox集成到应用程序中:

import wx
import sys

class AboutRecipeFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(AboutRecipeFrame, self).__init__(*args,
                                               **kwargs)

        # Attributes
        self.panel = wx.Panel(self)

        # Setup Menus
        menubar = wx.MenuBar()
        helpmenu = wx.Menu()
        helpmenu.Append(wx.ID_ABOUT, "About")
        menubar.Append(helpmenu, "Help")
        self.SetMenuBar(menubar)

        # Setup StatusBar
        self.CreateStatusBar()
        self.PushStatusText("See About in the Menu")

        # Event Handlers
        self.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT)

    def OnAbout(self, event):
        """Show the about dialog"""
        info = wx.AboutDialogInfo()

        # Make a template for the description
        desc = ["\nwxPython Cookbook Chapter 5\n",
                "Platform Info: (%s,%s)",
                "License: Public Domain"]
        desc = "\n".join(desc)

        # Get the platform information
        py_version = [sys.platform,
                      ", python ",
                      sys.version.split()[0]]
        platform = list(wx.PlatformInfo[1:])
        platform[0] += (" " + wx.VERSION_STRING)
        wx_info = ", ".join(platform)

        # Populate with information
        info.SetName("AboutBox Recipe")
        info.SetVersion("1.0")
        info.SetCopyright("Copyright (C) Joe Programmer")
        info.SetDescription(desc % (py_version, wx_info))

        # Create and show the dialog
        wx.AboutBox(info)

class AboutRecipeApp(wx.App):
    def OnInit(self):
        self.frame = AboutRecipeFrame(None, 
                                      title="AboutDialog", 
                                      size=(300,200))
        self.SetTopWindow(self.frame)
        self.frame.Show()

        return True

if __name__ == "__main__":
    app = AboutRecipeApp(False)
    app.MainLoop()

它是如何工作的...

在这个示例中,我们创建了一个非常简单但功能完整的应用程序,用于创建和显示关于对话框。因此,让我们回顾一下上述代码中的重要部分。

首先,让我们看看在AboutRecipeFrame类中设置菜单的部分。标准的应用程序关于对话框是通过菜单项显示的。在 Windows 和 GTK Linux 上,这个菜单项位于帮助菜单下;在 Macintosh OS X 上,这个菜单项位于应用程序菜单下。由于我们为我们的关于菜单项分配了wx.ID_ABOUT库存 ID,wxPython 会自动为我们处理这些平台差异,这个 ID 让 wx 知道该菜单项是一个标准的关于信息菜单项。

本食谱的下一部分且最为重要的部分是OnAbout菜单事件处理器。当我们的“关于”菜单条目被激活时,将会调用此方法,这也是我们通过调用AboutBox函数创建并显示“关于”对话框的地方。AboutBox函数需要一个包含所有我们希望在创建的对话框中显示的信息的AboutDialogInfo对象。

AboutDialogInfo 对象提供了多种方法来设置对话框可以支持的不同数据字段。这些方法都是简单的设置方法,它们接受字符串或字符串列表作为参数。在本食谱中,我们使用了其中的四种方法:

  1. SetName 函数用于获取应用程序的名称。此字符串将在对话框的标题栏中显示,并在主内容区域的第一行中显示。

  2. SetVersion 用于设置和显示应用程序的版本号。这将在主内容区域中的应用程序名称之后显示。

  3. SetCopyright 设置版权信息字段。关于这个方法需要注意的特殊之处是,如果字符串中包含一个(C),这将自动转换为版权符号 ©。

  4. SetDescription 是主要描述字段,可以包含关于应用的任何任意信息。

我们最后做的事情是显示“关于”对话框。这相当简单。我们只需要调用我们创建的AboutDialogInfo参数来调用wx.AboutBox函数。

还有更多...

AboutDialogInfo 对象支持许多额外的字段,用于其他特殊类型的数据和自定义 AboutBox。wxPython 在三个主要平台(MSW、GTK、OSX)上提供了关于对话框的原生实现。然而,只有 GTK 版本的 AboutBox 才原生支持 AboutDialogInfo 所支持的所有额外字段。如果 AboutDialogInfo 包含任何原生对话框不支持的字段,wxPython 将自动切换到通用版本的对话框。如果你想在应用程序中保持原生外观和感觉,这可能会成为一个问题。因此,以下是其他可用的 AboutDialogInfo 字段列表,以及哪些字段会在 Windows 和 OS X 上导致使用通用对话框:

其他 AboutDialogInfo 字段 描述
SetArtists(list_of_strings) 用于归功于应用程序的图形艺术家。
SetDevelopers(list_of_strings) 用于认可应用程序的开发者。
SetDocWriters(list_of_strings) 用于归功于应用程序的文档编写者。
SetIcon(icon) 自定义对话框的图标。默认为应用程序图标(仅限 GTK)
SetLicense(license_string) 用于显示应用程序的长许可文本(仅限 GTK)。
SetTranslators(list_of_strings) 用于认可应用程序的翻译者。
SetWebSite(url_string) 在对话框中创建指向网站的链接(仅限 GTK)。

参见

  • 第一章中的使用库存 ID配方,在使用 wxPython 入门中解释了内置 ID 的用法。

  • 第三章中的添加菜单和菜单栏配方,用户界面基本构建块包含了关于创建菜单并将它们添加到框架的MenuBar中的详细信息。

第六章:从用户检索信息

在本章中,我们将涵盖:

  • 使用FileDialog选择文件

  • 使用 FindReplaceDialog 搜索文本

  • 使用 ImageDialog 获取图片

  • 使用打印对话框

简介

能够从用户那里检索信息是任何应用程序的基本部分。对话框是检索用户信息的一种方式;大多数桌面应用程序都使用一些常见的对话框来执行诸如打开、保存和打印文件等任务。

有两种主要的对话框类型:模态对话框和模式对话框。模态对话框是在显示时阻止并禁用与其父窗口或应用程序中所有其他窗口(在应用程序模态对话框的情况下)的交互的对话框。模态对话框用于程序在继续其下一个任务之前必须从用户那里检索数据的情况。另一方面,模式对话框的行为类似于框架。当显示模式对话框时,应用程序中的其他窗口仍然可访问。当关闭模式对话框时,通常会将一个事件发送到其父窗口,以通知它对话框已关闭。

wxPython 提供了许多内置对话框,可以满足几乎所有常见情况的需求。因此,让我们通过本章中的食谱来查看一些这些常见对话框的实际应用。

使用 FileDialog 选择文件

允许用户打开和保存文件是许多应用最基本的功能之一。为了提供这一功能,通常需要赋予用户选择打开哪些文件、为新文件命名以及保存新文件的位置的能力。FileDialog 可以在您的应用中扮演这一角色。本食谱创建了一个简单的文本编辑应用,可以打开和保存文本文件,以展示如何使用 FileDialog

如何做到这一点...

在这里,我们将创建一个完整的文本编辑器应用程序:

import wx

class FileEditorApp(wx.App):
    def OnInit(self):
        self.frame = FileEditorFrame(None,
                                     title="File Editor")
        self.frame.Show()
        return True

我们的主要应用程序窗口在此定义,并包括一个框架(Frame)、文本控件(TextCtrl)和菜单栏(MenuBar):

class FileEditorFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(FileEditorFrame, self).__init__(*args, **kwargs) 

        # Attributes
        self.file = None
        style = style=wx.TE_MULTILINE|wx.TE_RICH2
        self.txtctrl = wx.TextCtrl(self, style=style)

        # Setup
        self._SetupMenus()

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.txtctrl, 1, wx.EXPAND)
        self.SetSizer(sizer)

        # Event Handlers
        self.Bind(wx.EVT_MENU, self.OnOpen, id=wx.ID_OPEN)
        self.Bind(wx.EVT_MENU, self.OnSave, id=wx.ID_SAVE)
        self.Bind(wx.EVT_MENU, self.OnSave, id=wx.ID_SAVEAS)
        self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT)
        self.Bind(wx.EVT_CLOSE, self.OnExit)

    def _SetupMenus(self):
        """Make the frames menus"""
        menub = wx.MenuBar()
        fmenu = wx.Menu()
        fmenu.Append(wx.ID_OPEN, "Open\tCtrl+O")
        fmenu.AppendSeparator()
        fmenu.Append(wx.ID_SAVE, "Save\tCtrl+S")
        fmenu.Append(wx.ID_SAVEAS, "Save As\tCtrl+Shift+S")
        fmenu.AppendSeparator()
        fmenu.Append(wx.ID_EXIT, "Exit\tCtrl+Q")
        menub.Append(fmenu, "File")
        self.SetMenuBar(menub)

这里是我们在 Frame 的 MenuBar 上添加的MenuItems的事件处理器。这些事件处理器主要用于将任务委托给执行用户请求的操作的方法。

    #---- Event Handlers ----#

    def OnOpen(self, event):
        """Handle Open"""
        if event.GetId() == wx.ID_OPEN:
            self.DoOpen()
        else:
            event.Skip()

    def OnSave(self, event):
        """Handle Save/SaveAs"""
        evt_id = event.GetId()
        if evt_id in (wx.ID_SAVE,
                      wx.ID_SAVEAS):
            if self.file:
                self.Save(self.file)
            else:
                self.DoSaveAs()
        else:
            event.Skip()

    def OnExit(self, event):
        """Handle window close event"""
        # Give warning about unsaved changes
        if self.txtctrl.IsModified():
            message = ("There are unsaved changes.\n\n"
                       "Would you like to save them?")
            style = wx.YES_NO|wx.ICON_WARNING|wx.CENTRE
            result = wx.MessageBox(message,
                                   "Save Changes?",
                                   style=style)
            if result == wx.YES:
                if self.file is None:
                    self.DoSaveAs()
                else:
                    self.Save(self.file)
        event.Skip()

    #---- End Event Handlers ----#

    #---- Implementation ----#

在此,在DoOpen方法中,我们首次使用FileDialogOPEN模式,允许用户选择他们希望打开的文件:

    def DoOpen(self):
        """Show file open dialog and open file"""
        wildcard = "Text Files (*.txt)|*.txt"
        dlg = wx.FileDialog(self,
                            message="Open a File",
                            wildcard=wildcard,
                            style=wx.FD_OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            with open(path, "rb") as handle:
                text = handle.read()
                self.txtctrl.SetValue(text)
                self.file = path
        dlg.Destroy()

DoSaveAs 中,我们看到了 FileDialog 的第二次使用,通过以 SAVE 模式创建对话框,允许用户选择保存文件的位置。

    def DoSaveAs(self):
        """Show SaveAs dialog"""
        wildcard = "Text Files (*.txt)|*.txt"
        dlg = wx.FileDialog(self,
                            message="Save As",
                            wildcard=wildcard,
                            style=wx.FD_SAVE
                                  |wx.FD_OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            self.Save(path)
            self.file = path
        dlg.Destroy()

    def Save(self, path):
        """Save the file"""
        with open(path, "wb") as handle:
            text = self.txtctrl.GetValue()
            handle.write(text)
            self.txtctrl.SetModified(False)

    #---- End Implementation ----#

#---- Main Execution ----#
if __name__ == "__main__":
    app = FileEditorApp(False)
    app.MainLoop()

它是如何工作的...

只为了提供一个感觉,看看你能在 wxPython 中多快地创建一个可用的应用程序,让我们来看看上面的配方。大约 100 行代码,我们就基本实现了一个 Windows 记事本克隆版。由于这个配方是关于FileDialog的,我们就专注于我们的文本编辑应用程序的DoOpenDoSaveAs方法,看看它是如何工作的。

FileDialog 有两种基本模式:打开和保存。对话框的模式取决于它创建时使用的样式标志。我们的 DoOpen 方法使用 FD_OPEN 样式标志来创建它,这使得它处于打开模式。打开模式与保存模式的不同之处在于,它只允许你选择一个文件,而不能输入名称来创建一个新的文件。

在我们在这个应用程序中使用的两个FileDialog中,我们都使用了相同的可选参数来创建它。wildcards参数接受一个特殊格式的字符串来指定对话框中的文件过滤器列表。这个字符串必须按照以下格式进行格式化:

“All Files (*)|*|Text Files (*.txt;*.in)|*.txt;*.in”

该字符串中的字段解释如下:

Description1|wildcard1|Description2|wildcard1;wildcard2

每个字段都是一个描述,后面跟着管道字符作为分隔符,然后是wx.ID_OK通配符来与该描述关联。多个通配符由分号分隔。

一旦设置了对话框,其使用方法非常简单。只需使用 ShowModal 来显示它。然后,如果用户确认关闭它,我们只需调用对话框的 GetPath 方法,以便获取用户在对话框中选择的或输入的路径。

还有更多...

FileDialog的构造函数接受多个参数以自定义其行为;有关如何设置FileDialog的更多信息,请参阅下文。

默认路径

FileDialog有几个我们应用中没有使用的额外参数,可以用来自定义其初始状态。第一个是defaultDir参数,它接受一个目录路径作为值。这个路径必须存在,并且将确保对话框以该目录选中时显示。另一个额外的参数是defaultFile,它接受一个文件名作为值。这将被设置为对话框文件名字段的默认值。

样式标志

样式标志及其描述如下表所示:

样式标志 描述
wx.FD_DEFAULT_STYLE wx.FD_OPEN 相同
wx.FD_OPEN 创建为打开对话框。不能与 wx.FD_SAVE 结合使用。
wx.FD_SAVE 创建一个保存对话框。不能与 wx.FD_OPEN 结合使用。
wx.FD_OVERWRITE_PROMPT 确认路径已存在时的提示。仅适用于保存对话框。
wx.FD_FILE_MUST_EXIST 允许用户仅选择实际存在的文件。仅适用于打开对话框。
wx.FD_MULTIPLE 允许多个文件被选中。仅适用于打开对话框。应使用对话框的GetPaths方法来获取选中路径的列表。
wx.FD_PREVIEW 显示所选文件的预览。
wx.FD_CHANGE_DIR 将当前工作目录更改为用户选择文件的位置。如果不使用defaultDir参数,下次打开对话框时,它将打开到最后一次使用的位置。

参见

  • 第一章中的使用库存 ID配方,在使用 wxPython 入门中解释了内置 ID 的用法。

  • 第二章中的“处理事件”食谱涵盖了事件处理的基本知识。

  • 第三章中的 添加菜单和菜单栏 菜谱,用户界面基本构建块 讨论了如何将菜单添加到框架中。

使用 FindReplaceDialog 搜索文本

FindReplaceDialog 是一个常见的对话框,用于从用户那里获取信息以便在应用程序中执行查找和替换操作。FindReplaceDialog 总是作为一个无模式对话框使用,当其按钮被点击时,会发出事件通知父窗口用户希望执行的操作。本食谱将扩展之前的食谱(FileDialog),展示如何使用 FindReplaceDialog 将查找和替换功能添加到应用程序中。

如何做到这一点...

在这里,我们将展示我们如何通过FindReplaceDialog:FileEditorFrame进行子类化以添加查找和替换功能:

import wx
# FileDialog Recipe sample module
import filedialog

class FindReplaceEditorFrame(filedialog.FileEditorFrame):
    def __init__(self, parent, *args, **kwargs):
        super(FindReplaceEditorFrame, self).__init__(*args,
                                                     **kwargs)

        # Attributes
        self.finddlg = None
        self.finddata = wx.FindReplaceData()

        # Setup
        menub = self.GetMenuBar()
        editmenu = wx.Menu()
        editmenu.Append(wx.ID_FIND, "Find\tCtrl+F")
        editmenu.Append(wx.ID_REPLACE, "Replace\tCtrl+R")
        menub.Append(editmenu, "Edit")

        # Event Handlers
        self.Bind(wx.EVT_MENU,
                  self.OnFindMenu,
                  id=wx.ID_FIND)
        self.Bind(wx.EVT_MENU,
                  self.OnFindMenu,
                  id=wx.ID_REPLACE)
        self.Bind(wx.EVT_FIND, self.OnFind)
        self.Bind(wx.EVT_FIND_NEXT, self.OnFind)
        self.Bind(wx.EVT_FIND_REPLACE, self.OnReplace)
        self.Bind(wx.EVT_FIND_REPLACE_ALL, self.OnReplaceAll)
        self.Bind(wx.EVT_FIND_CLOSE, self.OnFindClose)

此方法是一个辅助方法,根据用户从菜单中选择的行为,以正确的模式创建FindReplaceDialog

    def _InitFindDialog(self, mode):
        if self.finddlg:
            self.finddlg.Destroy()

        style = (wx.FR_NOUPDOWN
                 |wx.FR_NOMATCHCASE
                 |wx.FR_NOWHOLEWORD)
        if mode == wx.ID_REPLACE:
            style |= wx.FR_REPLACEDIALOG
            title = "Find/Replace"
        else:
            title = "Find"
        dlg = wx.FindReplaceDialog(self,
                                   self.finddata,
                                   title,
                                   style)
        self.finddlg = dlg

    # ---- Event Handlers ----#

这个第一个事件处理器用于处理当选择菜单项时的事件,并将用于创建和显示相应的FindReplaceDialog:版本:

    def OnFindMenu(self, event):
        evt_id = event.GetId()
        if evt_id in (wx.ID_FIND, wx.ID_REPLACE):
            self._InitFindDialog(evt_id)
            self.finddlg.Show()
        else:
            event.Skip()

接下来的四个事件处理器处理由FindReplaceDialog在用户操作后生成的事件:

    def OnFind(self, event):
        """Find text"""
        findstr = self.finddata.GetFindString()
        if not self.FindString(findstr):
            wx.Bell() # beep at the user for no match

    def OnReplace(self, event):
        """Replace text"""
        rstring = self.finddata.GetReplaceString()
        fstring = self.finddata.GetFindString()
        cpos = self.GetInsertionPoint()
        start, end = cpos, cpos
        if fstring:
            if self.FindString(fstring):
                start, end = self.txtctrl.GetSelection()
        self.txtctrl.Replace(start, end, rstring)

    def OnReplaceAll(self, event):
        """Do a replace all"""
        rstring = self.finddata.GetReplaceString()
        fstring = self.finddata.GetFindString()
        text = self.txtctrl.GetValue()
        newtext = text.replace(fstring, rstring)
        self.txtctrl.SetValue(newtext)

    def OnFindClose(self, event):
        if self.finddlg:
            self.finddlg.Destroy()
            self.finddlg = None

    #---- End Event Handlers ----#

    #---- Implementation ----#

最后,这里有一个在TextCtrl中搜索给定字符串并设置选择的非常简单的方法:

    def FindString(self, findstr):
        """Find findstr in TextCtrl and set selection"""
        text = self.txtctrl.GetValue()
        csel = self.txtctrl.GetSelection()
        if csel[0] != csel[1]:
            cpos = max(csel)
        else:
            cpos = self.txtctrl.GetInsertionPoint()

        if cpos == self.txtctrl.GetLastPosition():
            cpos = 0

        # Do a simple case insensitive search 
        # to find the next match
        text = text.upper()
        findstr = findstr.upper()
        found = text.find(findstr, cpos)
        if found != -1:
            end = found + len(findstr)
            self.txtctrl.SetSelection(end, found)
            self.txtctrl.SetFocus()
            return True
        return False

运行前面的代码将会显示如下窗口:

如何做...

它是如何工作的...

在这个菜谱中,我们使用了在上一道菜谱中创建的FileEditorFrame类,并通过使用FindReplaceDialog扩展了其查找和替换功能。因此,让我们通过查看我们添加到这个类中的内容,从上到下了解我们是如何利用FindReplaceDialog的。

在我们的 FindReplaceEditorFrame 类的 __init__ 方法中,我们添加了两个实例属性,finddlgfinddata。由于 FindReplaceDialog 是非模态的,我们需要在我们的类中跟踪它,以便我们可以在稍后适当地清理它,确保它在创建时将被分配给 finddlg 属性。第二个属性 finddata 保存了对 FindReplaceData 的引用,该数据用于初始化对话框,以及用于在对话框和其父窗口之间传递数据。我们保留对这个项目的引用有两个原因,首先,它允许方便地访问对话框的标志和用户输入的查找和替换字段字符串,其次,通过每次使用相同的 FindReplaceData 对象,对话框将使用用户上次使用时相同的设置进行初始化。在 __init__ 中需要注意的最后一件事是事件绑定:我们将五个 FindReplaceDialog 可以在用户与之交互时发出的事件绑定到它。

下一个新方法是 _InitFindDialog 方法。这个方法是我们用来响应 查找替换 菜单项事件以初始化 FindReplaceDialog 的。由于我们的应用程序只将支持简单的单向、不区分大小写的搜索,我们使用适当的样式标志禁用了对话框中的所有额外选项。然后,如果我们处于替换模式,我们简单地使用 FR_REPLACEDIALOG 标志创建对话框,如果不处于替换模式,则不使用该标志,最后将新的对话框实例分配给我们的 finddlg 属性。

下一个需要查看的部分是我们的 FindReplaceDialog 事件处理器。这里我们处理用户通过对话框发起的请求操作。OnFind 处理用户在对话框中点击 查找查找下一个 按钮的情况。在这里,我们首先通过使用我们的 finddata 属性来访问它,获取到对话框中输入的字符串。然后我们在基类的 TextCtrl 文本中进行简单搜索,如果找到匹配项则选择它,如果没有找到匹配项则使用 wx.Bell 使计算机对用户发出蜂鸣声。

OnReplace 方法在点击 FindReplaceDialog 中的 Replace 按钮时被调用。在这里,我们从 FindReplaceData 中获取输入的查找和替换字符串。然后我们尝试找到匹配项,并用输入的替换字符串替换该匹配项。OnReplaceAll 方法在点击对话框的 Replace All 按钮时被调用,它基本上与 OnReplace 做相同的事情,但将操作应用于 TextCtrl 文本中的所有匹配项。

最后的事件处理程序是 OnFindClose。当用户关闭 FindReplaceDialog 时会调用此函数。我们需要处理这个事件,以便通过调用其上的 Destroy 方法来清理对话框。就这样。现在我们有一个具有查找和替换功能文本编辑器应用程序了!

还有更多...

为了简化,这个配方禁用了对话框的额外查找选项。当在对话框中选择这些选项时,可以通过使用FindReplaceData对象来检查它们,就像查找和替换字符串一样。它将在GetFlags返回的值中设置所选选项的标志,这是一个FindReplaceData标志的位掩码。由于这些标志和对话框样式标志的命名方式,了解它们各自是什么可能会有些困惑,因此请参考以下两个表格来区分这两组不同但名称相似的标志集。

FindReplaceDialog 样式标志

这些标志是应该传递给对话框构造函数的样式参数的标志:

Flags 描述
wx.FR_NOMATCHCASE 禁用“区分大小写”复选框
wx.FR_NOUPDOWN 禁用“上”和“下”单选按钮
wx.FR_NOWHOLEWORD 禁用“全字”复选框
wx.FR_REPLACEDIALOG 在替换模式下创建对话框

FindReplaceData 标志

以下标志是可以在FindReplaceData中设置的标志,用于设置对话框的初始状态以及识别用户选择的查找偏好。

标志 描述
wx.FR_DOWN “向下”单选按钮被选中
wx.FR_MATCHCASE “匹配大小写”复选框被选中
wx.FR_WHOLEWORD “全词”复选框被选中

参见

  • 请参阅使用 FileDialog 选择文件的配方,以了解此配方扩展的基本示例。

  • 第二章中的理解事件传播配方,响应事件包含了更多关于事件如何传递到不同窗口的信息。

使用 ImageDialog 获取图片

ImageDialog 是由 wx.lib.imagebrowser 模块提供的一个自定义对话框类。它在目的上与默认的 FileDialog 类似,但专门用于允许用户选择和预览图像。本菜谱展示了如何使用此对话框检索用户选择的图像并将其加载到 StaticBitmap 中,以便在应用程序的主窗口中显示。

如何做到这一点...

在这里,我们将创建一个非常简单的图片查看器应用程序,允许用户使用 ImageDialog 选择要查看的图片:

import wx
import wx.lib.imagebrowser as imagebrowser

class ImageDialogApp(wx.App):
    def OnInit(self):
        self.frame = ImageDialogFrame(None,
                                       title="ImageDialog")
        self.frame.Show()
        return True

class ImageDialogFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(ImageDialogFrame, self).__init__(*args, 
                                               **kwargs)wx.Frame.__init__(self, parent, *args, **kwargs)

        # Attributes
        self.panel = ImageDialogPanel(self)

class ImageDialogPanel(wx.Panel):
    def __init__(self, parent, *args, **kwargs):
        super(ImageDialogPanel, self).__init__(*args, 
                                               **kwargs)wx.Panel.__init__(self, parent, *args, **kwargs)

        # Attributes
        self.lastpath = None
        self.bmp = wx.StaticBitmap(self)
        self.btn = wx.Button(self, label="Choose Image")

        # Layout
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        vsizer.Add(self.bmp, 0, wx.ALIGN_CENTER)
        vsizer.AddSpacer((5, 5))
        vsizer.Add(self.btn, 0, wx.ALIGN_CENTER)
        hsizer.AddStretchSpacer()
        hsizer.Add(vsizer, 0, wx.ALIGN_CENTER)
        hsizer.AddStretchSpacer()
        self.SetSizer(hsizer)

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnShowDialog, self.btn)

    def OnShowDialog(self, event):
        # Create the dialog with the path cached
        # from the last time it was opened
        dlg = imagebrowser.ImageDialog(self, self.lastpath)
        if dlg.ShowModal() == wx.ID_OK:
            # Save the last used path
            self.lastpath = dlg.GetDirectory()
            imgpath = dlg.GetFile()
            bitmap = wx.Bitmap(imgpath)
            if bitmap.IsOk():
                self.bmp.SetBitmap(bitmap)
                self.Layout()
                self.bmp.Refresh()
        dlg.Destroy()

if __name__ == '__main__':
    app = ImageDialogApp(False)
    app.MainLoop()

运行上一段代码并点击选择图片按钮,将会显示以下对话框:

如何做...

它是如何工作的...

在这个菜谱中,我们创建了一个简单的图像查看器应用程序,允许用户使用ImageDialog选择计算机硬盘上的图像,并在应用程序窗口中显示该图像。

这个应用程序遵循了大多数简单应用程序的常见模式。因此,让我们详细看看我们是如何使用ImageDialog的。首先,我们必须导入wx.lib.imagebrowser模块,因为ImageDialog不是标准 wx 模块的一部分。在我们的ImageDialogFrame类中,我们添加了三个实例属性。第一个是用来保存ImageDialog上次使用的路径。我们这样做是为了提高应用程序的可用性,以便我们可以在用户下次打开应用程序时打开对话框到他们上次使用的最后一个路径。第二个属性是一个StaticBitmap对象,我们将使用它来显示用户通过ImageDialog选择的图像。请注意,我们在这个例子中使用了StaticBitmap以简化示例。为了更好地支持更大尺寸的图像,最好是我们自己将图像绘制在Panel上。这种方法将在第八章,屏幕绘图这一主题中介绍。最后一个属性只是一个按钮,它将用来触发显示ImageDialog的事件。

这个应用程序遵循了大多数简单应用程序的常见模式。因此,让我们详细看看我们是如何使用ImageDialog的。首先,我们必须导入wx.lib.imagebrowser模块,因为ImageDialog不是标准 wx 模块的一部分。在我们的ImageDialogFrame类中,我们添加了三个实例属性。第一个是用来保存ImageDialog上次使用的路径。我们这样做是为了提高应用程序的可用性,以便我们可以在用户下次打开应用程序时打开对话框到他们上次使用的最后一个路径。第二个属性是一个StaticBitmap对象,我们将使用它来显示用户通过ImageDialog选择的图像。请注意,我们在这个例子中使用了StaticBitmap以简化示例。为了更好地支持更大尺寸的图像,最好是我们自己将图像绘制在Panel上。这种方法将在第八章,屏幕绘图这一主题中介绍。最后一个属性只是一个按钮,它将被用来触发显示ImageDialog的事件。

在本食谱中,我们的 OnShowDialog 方法创建 ImageDialog 并将其初始化为上次使用的路径。第一次使用时,它将是 None,默认为当前工作目录。然后显示对话框,以便用户导航并选择要显示的图像。如果他们点击对话框的 打开 按钮,对话框将返回 wx.ID_OK。在此阶段,我们首先获取并保存对话框最后所在的目录引用,以便下次显示对话框时可以恢复它。然后剩下的就是创建位图并调用 StaticBitmapSetBitmap 方法来更改显示的图像。之后,必须调用 Panel 上的 Layout,以确保调整器可以适应新的 Bitmap 的大小,然后我们最终调用 StaticBitmap 上的 Refresh 来确保它被完全重绘。

还有更多...

在当前版本的 ImageDialog 中,唯一可用的其他选项是能够更改支持的文件过滤器列表。这可以通过向对话框的 ChangeFileTypes 方法传递一个包含文件类型描述和通配符字符串的元组列表来实现。

dlg.ChangeFileTypes([('png, '*.png'), ('jpeg', '*.jpg')])

参见

  • 第一章中的 使用位图 菜单,在 wxPython 入门 部分提供了使用位图和 StaticBitmap 类的额外示例。

使用打印对话框

将打印支持添加到应用程序可能是一项艰巨的任务,因为需要处理许多任务。这包括选择和配置打印机、将屏幕上的演示文稿转换为纸张,以及最终将数据发送到打印机。

在 wxPython 中,有三个与打印相关的对话框类:PageSetupDialogPreviewFramePrinter类。除了这些类之外,还有一些必须与这些对话框一起使用的支持类,以便将打印支持添加到应用程序中。本菜谱展示了如何使用 wx 打印框架的一些基本知识,通过创建一个封装三个打印对话框使用的类,允许应用程序打印Bitmap

如何做到这一点...

为了简化并压缩在应用程序中支持打印所需的各种不同步骤,我们将首先定义一个类,将不同的任务封装成几个简单的调用方法:

class BitmapPrinter(object):
    """Manages PrintData and Printing"""
    def __init__(self, parent):
        """Initializes the Printer
        @param parent: parent window
        """
        super(BitmapPrinter, self).__init__()

        # Attributes
        self.parent = parent
        self.print_data = wx.PrintData()

    def CreatePrintout(self, bmp):
        """Creates a printout object
        @param bmp: wx.Bitmap
        """
        assert bmp.IsOk(), "Invalid Bitmap!"
        data = wx.PageSetupDialogData(self.print_data)
        return BitmapPrintout(bmp, data)

PageSetup 方法处理打印机设置对话框的显示,并将设置存储在 print_data 属性中:

    def PageSetup(self):
        """Show the PrinterSetup dialog"""
        # Make a copy of our print data for the setup dialog
        dlg_data = wx.PageSetupDialogData(self.print_data)
        print_dlg = wx.PageSetupDialog(self.parent, dlg_data)
        if print_dlg.ShowModal() == wx.ID_OK:
            # Update the printer data with the changes from
            # the setup dialog.
            newdata = dlg_data.GetPrintData()
            self.print_data = wx.PrintData(newdata)
            paperid = dlg_data.GetPaperId()
            self.print_data.SetPaperId(paperid)
        print_dlg.Destroy()
)

在预览中,我们创建PrintPreview对话框以预览打印输出将呈现的样子:

    def Preview(self, bmp):
        """Show the print preview
        @param bmp: wx.Bitmap
        """
        printout = self.CreatePrintout(bmp)
        printout2 = self.CreatePrintout(bmp)
        preview = wx.PrintPreview(printout, printout2,
                                  self.print_data)
        preview.SetZoom(100)
        if preview.IsOk():
            pre_frame = wx.PreviewFrame(preview,
                                        self.parent,
                                        "Print Preview")
            # The default size of the preview frame
            # sometimes needs some help.
            dsize = wx.GetDisplaySize()
            width = self.parent.GetSize()[0]
            height = dsize.GetHeight() - 100
            pre_frame.SetInitialSize((width, height))
            pre_frame.Initialize()
            pre_frame.Show()
        else:
            # Error
            wx.MessageBox("Failed to create print preview",
                          "Print Error",
                          style=wx.ICON_ERROR|wx.OK)

最后,在Print方法中,我们显示Printer对话框,允许用户请求打印一个Bitmap,并将其发送到打印机进行打印:

    def Print(self, bmp):
        """Prints the document"""
        pdd = wx.PrintDialogData(self.print_data)
        printer = wx.Printer(pdd)
        printout = self.CreatePrintout(bmp)
        result = printer.Print(self.parent, printout)
        if result:
            # Store copy of print data for future use
            dlg_data = printer.GetPrintDialogData()
            newdata = dlg_data.GetPrintData()
            self.print_data = wx.PrintData(newdata)
        elif printer.GetLastError() == wx.PRINTER_ERROR:
            wx.MessageBox("Printer error detected.",
                          "Printer Error",
                          style=wx.ICON_ERROR|wx.OK)
        printout.Destroy()

在这里,我们将实现用于打印 BitmapsPrintout 对象。Printout 对象是负责管理打印任务并将位图绘制到打印机设备上下文的对象:

class BitmapPrintout(wx.Printout):
    """Creates an printout of a Bitmap"""
    def __init__(self, bmp, data):
        super(BitmapPrintout, self).__init__()wx.Printout.__init__(self)

        # Attributes
        self.bmp = bmp
        self.data = data

    def GetPageInfo(self):
        """Get the page range information"""
        # min, max, from, to # we only support 1 page
        return (1, 1, 1, 1)

    def HasPage(self, page):
        """Is a page within range"""
        return page <= 1

    def OnPrintPage(self, page):
        """Scales and Renders the bitmap
        to a DC and prints it
        """
        dc = self.GetDC() # Get Device Context to draw on

        # Get the Bitmap Size
        bmpW, bmpH = self.bmp.GetSize()

        # Check if we need to scale the bitmap to fit
        self.MapScreenSizeToPageMargins(self.data)
        rect = self.GetLogicalPageRect()
        w, h = rect.width, rect.height
        if (bmpW > w) or (bmpH > h):
            # Image is large so apply some scaling
            self.FitThisSizeToPageMargins((bmpW, bmpH),
                                          self.data)
            x, y = 0, 0
        else:
            # try to center it
            x = (w - bmpW) / 2
            y = (h - bmpH) / 2

        # Draw the bitmap to DC
        dc.DrawBitmap(self.bmp, x, y)

        return True

它是如何工作的...

BitmapPrinter 类封装了应用程序可能需要支持的三个主要打印相关任务:打印机设置、打印预览和打印。这个类是想要允许打印 Bitmaps 的应用程序使用的接口,用于其所有打印需求。应用程序所需的一切只是一个 Bitmap,而它需要做的只是使用这三个方法之一:PageSetupPreviewPrint。那么,让我们来看看这个类和这三个方法是如何工作的。

构造函数接受一个参数用于父窗口。这将被用作所有对话框的父窗口。这通常是一个应用程序的主窗口。我们还在构造函数中创建并存储了一个PrintData对象的引用。所有打印对话框都以一种或另一种形式使用PrintData。这允许我们在用户使用其中一个对话框时保存用户可能做出的任何打印配置更改。

PageSetup 用于创建和显示 PageSetupDialog。要使用 PageSetupDialog,我们首先通过将其构造函数传递我们的 PrintData 对象来创建一个 PageSetupDialogData 对象,这样它将使用我们数据对象中可能已经持久化的任何设置。然后我们只需通过传递 PageSetupDialogData 对象来创建对话框。如果对话框通过“确定”按钮关闭,我们随后从对话框中获取 PrintData 并制作一个副本以存储。制作副本非常重要,因为当 PageSetupDialog 被销毁时,它将删除数据。

预览功能会创建打印输出的预览效果,并通过PreviewFrame显示。PreviewFrame需要一个PrintPreview对象。要创建PrintPreview对象,必须传入两个Printout对象和一个PrintData对象。Printout对象负责实际渲染打印机将要打印的内容。当我们到达BitmapPrintout类时,我们将回到关于Printout如何工作的细节。第一个Printout对象用于PreviewFrame,第二个对象用于用户点击PreviewFrame的打印按钮时的实际打印。

Print 函数创建一个 Printer 对象,当调用其 Print 方法时将显示打印机对话框。与 Preview 对象类似,Printer 对象是在一些 PrintData 和一个 Printout 对象的实例的基础上创建的。当点击打印对话框中的打印按钮时,它将使用 Printout 对象来告诉物理打印机在纸上绘制什么内容。

BitmapPrintout 类实现了用于一次性将单个位图打印到一张纸上的 Printout 对象。Printout 对象必须始终被子类化,以便实现需要打印的数据的应用特定要求,因为基类仅提供了一个接口,该接口在子类中需要重写虚拟方法。在我们的类中,我们重写了以下三个方法:GetPageInfoHasPageOnPrintPage。前两个方法用于返回将要打印的页数信息;由于我们只支持一页,这些在这个菜谱中相当简单。OnPrintPage 方法是实际将绘图绘制到打印机设备上下文的方法。此方法被调用以绘制将要打印的每一页。

通过调用GetDC函数返回的设备上下文对象来绘制Printout。设备上下文的使用在第八章 屏幕绘图中有详细说明,因此为了简化问题,我们在这里只是将画布计算的缩放比例设置为尝试将图像居中在纸张上,然后使用 DC 的DrawBitmap方法将Bitmap绘制到设备上下文中。关于BitmapPrinter类的实际应用示例,请参阅本章附带的示例代码。

还有更多...

以下包含有关打印框架的一些附加信息。

打印输出

wx.Printout 对象拥有许多其他可覆盖的方法,这些方法可能对不同类型的文档有所帮助。下表是这些其他接口方法的一些参考。

接口方法 描述
OnBeginDocument(start, end) 在打印作业中每个文档副本的开始时调用。如果此方法被重写,必须在其中调用基类的该方法。
OnEndDocument() 在打印作业中打印每份文档的末尾被调用。如果此方法被重写,必须在其中调用基类方法。
OnBeginPrinting() 在打印作业开始时只调用一次。
OnEndPrinting() 在打印作业结束时只调用一次。
OnPreparePrinting() 在使用 Printout 对象之前被调用。通常在这里进行关于诸如文档页数等事项的计算。

Bug notice

在 wxPython 2.8 中存在一个 bug,无法从PageSetupPrint对话框的PrintData中检索页面方向(纵向或横向)。

参见

  • 第一章中的理解继承限制配方,wxPython 入门包含了如何覆盖 wxPython 类中虚拟方法的详细解释。

  • 第八章中的屏幕绘图配方,屏幕绘图讨论了设备上下文(DCs)及其绘图命令的使用。

第七章:窗口布局与设计

在本章中,我们将涵盖:

  • 使用 BoxSizer

  • 理解比例、旗帜和边框

  • 使用 GridBagSizer 布置控件

  • 标准对话框按钮布局

  • 使用 XML 资源

  • 创建自定义资源处理器

  • 使用 AuiFrameManager

简介

一旦你对应用程序界面的外观有了想法,就是时候将所有元素整合在一起了。能够将你的愿景转化为代码可能是一项棘手且常常繁琐的任务。窗口布局是在一个二维平面上定义的,其原点位于窗口的左上角。任何小部件的位置和尺寸,无论其在屏幕上的外观如何,都是基于矩形的。清楚地理解这两个基本概念对于理解和高效地使用工具包大有裨益。

在传统的旧应用中,窗口布局通常是通过为窗口内包含的所有控件设置显式的静态大小和位置来完成的。然而,这种方法可能相当受限,因为窗口将无法调整大小,它们可能在不同分辨率下无法适应屏幕,尝试支持本地化变得更加困难,因为标签和其他文本在不同语言中的长度会有所不同,原生控件在不同平台上通常会有不同的大小,这使得编写平台无关的代码变得困难,问题还有很多。

因此,你可能想知道这个问题的解决方案是什么。在 wxPython 中,首选的方法是使用Sizer类来定义和管理控件布局。Sizer类通过查询已添加到Sizer中的所有控件的建议最佳最小尺寸以及它们在可用空间增加时是否能够拉伸或不能拉伸的能力,来管理控件的大小和位置。例如,如果用户将对话框放大,Sizer 也会处理这种情况。Sizer 还处理跨平台小部件的差异,例如,GTK 上的按钮通常带有图标,并且通常比 Windows 或 OS X 上的按钮要大。使用Sizer来管理按钮布局将允许对话框的其余部分按比例正确地调整大小,以处理这种情况,而无需任何特定于平台的代码。

因此,让我们通过查看 wxPython 提供的用于简化此任务的众多工具,开始我们的窗口布局与设计之旅。

使用 BoxSizer

BoxSizerSizer 类中最基本的类。它支持单方向的布局——要么是垂直的列,要么是水平的行。尽管它是使用起来最基础的,但 BoxSizer 是最有用的 Sizer 类之一,并且与其他一些 Sizer 类型相比,它往往能产生更一致的跨平台行为。这个示例创建了一个简单的窗口,我们希望在窗口中堆叠两个文本控件,每个控件旁边都有一个标签。这将用来展示如何最简单地使用 BoxSizer 来管理窗口控件布局。

如何做到这一点...

在这里,我们定义我们的顶级框架,它将使用 BoxSizer 来管理其面板的大小:

class BoxSizerFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(BoxSizerFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = BoxSizerPanel(self)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize()

BoxSizerPanel 类是窗口层次结构的下一层,也是我们将执行控件主要布局的地方:

class BoxSizerPanel(wx.Panel):
    def __init__(self, parent, *args, **kwargs):
        super(BoxSizerPanel, self).__init__(*args, **kwargs)

        # Attributes
        self._field1 = wx.TextCtrl(self)
        self._field2 = wx.TextCtrl(self)

        # Layout
        self._DoLayout()

仅仅为了帮助减少__init__方法中的杂乱,我们将所有布局操作放在一个单独的_DoLayout方法中:

    def _DoLayout(self):
        """Layout the controls"""
        vsizer = wx.BoxSizer(wx.VERTICAL)
        field1_sz = wx.BoxSizer(wx.HORIZONTAL)
        field2_sz = wx.BoxSizer(wx.HORIZONTAL)

        # Make the labels
        field1_lbl = wx.StaticText(self, label="Field 1:")
        field2_lbl = wx.StaticText(self, label="Field 2:")

        # Make the first row by adding the label and field
        # to the first horizontal sizer
        field1_sz.AddSpacer(50)
        field1_sz.Add(field1_lbl)
        field1_sz.AddSpacer(5) # put 5px of space between
        field1_sz.Add(self._field1)
        field1_sz.AddSpacer(50)

        # Do the same for the second row
        field2_sz.AddSpacer(50)
        field2_sz.Add(field2_lbl)
        field2_sz.AddSpacer(5)
        field2_sz.Add(self._field2)
        field2_sz.AddSpacer(50)

        # Now finish the layout by adding the two sizers
        # to the main vertical sizer.
        vsizer.AddSpacer(50)
        vsizer.Add(field1_sz)
        vsizer.AddSpacer(15)
        vsizer.Add(field2_sz)
        vsizer.AddSpacer(50)

        # Finally assign the main outer sizer to the panel
        self.SetSizer(vsizer)

它是如何工作的...

之前的代码展示了如何通过程序创建一个简单的窗口布局的基本模式,使用 sizers 来管理控件。首先,让我们先看看BoxSizerPanel类的_DoLayout方法,因为在这个示例中,大部分的布局都发生在这里。

首先,我们创建了三个BoxSizer类:一个具有垂直方向,另外两个具有水平方向。我们希望这个窗口的布局需要我们使用三个BoxSizer类,原因如下。如果你将我们想要做的事情分解成简单的矩形,你会看到:

  1. 我们想要两个TextCtrl对象,每个对象旁边都有一个标签,这可以简单地理解为两个水平的矩形。

  2. 我们希望TextCtrl对象在窗口中垂直堆叠,这个窗口仅仅是一个包含其他两个矩形的垂直矩形。

这可以通过以下截图来说明(图中已绘制边界并添加标签以显示每个Panel的三个BoxSizers管理的区域):

如何工作...

在填充第一个水平布局器(field1_sz)的部分,我们使用了两个BoxSizer方法来添加项目到布局中。第一个是AddSpacer,正如其名,它简单地添加一定量的空空间到布局器的左侧。然后我们使用Add方法将我们的StaticText控件添加到空格器的右侧,并从这里继续添加其他项目以完成这一行。正如你所见,这些方法在布局器中从左到右添加项目。之后,我们再次在第二个水平布局器中对其他标签和TextCtrl执行同样的操作。

面板布局的最后部分是通过向垂直尺寸器添加两个水平尺寸器来完成的。这次,由于尺寸器是以VERTICAL方向创建的,因此项目是从上到下添加的。最后,我们使用PanelSetSizer方法将主外部的BoxSizer分配为Panel的尺寸器。

BoxSizerFrame 也使用 BoxSizer 来管理其 Panel 的布局。这里唯一的区别在于,我们使用了 Add 方法的 proportionflags 参数来告诉它让 Panel 扩展以使用整个可用空间。在设置 Frame 的布局管理器后,我们使用了它的 SetInitialSize 方法,该方法查询窗口的布局管理器及其子代,以获取和设置最佳最小尺寸来设置窗口。我们将在下一个菜谱中详细介绍这些其他参数及其影响。

还有更多...

以下包含关于向 sizer 布局中添加间隔和项目的一些额外信息。

间隔件

AddSpacer 方法将添加一个正方形形状的间隔,其宽度和高度均为 X 像素,并将其添加到 BoxSizer 中,其中 X 是传递给 AddSpacer 方法的值。通过将一个 tuple 作为 BoxSizerAdd 方法的第一个参数传递,可以添加其他尺寸的间隔。

someBoxSizer.Add((20,5))

这将为 sizer 添加一个 20x5 像素的间隔。这在您不想垂直空间增加得像水平空间那么多,或者相反的情况下很有用。

AddMany

AddMany 方法可以在一次调用中向 sizer 添加任意数量的项目。AddMany 接收一个包含值的 tuple 列表,这些值与 Add 方法期望的顺序相同。

someBoxSizer.AddMany([(staticText,),
                      ((10, 10),),
                      (txtCtrl, 0, wx.EXPAND)]))

这将在 sizer 中添加三个项目:前两个项目仅指定一个必需的参数,第三个项目指定了比例标志参数。

参见

  • 本章中关于理解比例、旗帜和边框的配方进一步详细介绍了SizerItems的行为属性。

理解比例、旗帜和边框

通过使用 sizer 的各种Add方法中的可选参数,可以控制由 sizer 管理的每个项目的相对比例、对齐方式和周围填充。如果不使用这些额外设置,sizer 中的所有项目将仅使用它们的“最佳”最小尺寸,并将对齐到 sizer 提供的矩形空间顶部左侧。这意味着当窗口大小调整时,控件不会拉伸或收缩。例如,在一个BoxSizer的水平行项目列表中,如果其中某个项目的宽度大于同一行中的其他项目,它们可能不会按照期望对齐(参见以下图表)。

理解比例、旗帜和边框

此图展示了当某些控件与相邻的矩形大小不同时可能出现的对齐问题。这是 GTK(Linux)上可能发生的一个实际问题的例子,因为其ComboBoxes通常比StaticTextCtrl高得多。所以,在其他平台上这两个控件可能看起来是正确居中对齐的,但在 Linux 上它们会看起来像这样。

本食谱将重新实现之前的食谱中的BoxSizerPanel,使用这些额外的Add参数来改进其布局,以便展示这些参数如何被用来影响调整器如何管理添加到其中的每个控件。

入门指南

在开始这个菜谱之前,请确保你已经复习了之前的菜谱,使用 BoxSizer,因为在这个菜谱中,我们将修改其_DoLayout方法以定义一些 sizers 应该应用到其布局中的额外行为。

如何做到这一点...

在这里,我们将对SizerItems的比例、标志和边框进行一些修改,以改变布局的行为:

    def _DoLayout(self):
        """Layout the controls"""
        vsizer = wx.BoxSizer(wx.VERTICAL)
        field1_sz = wx.BoxSizer(wx.HORIZONTAL)
        field2_sz = wx.BoxSizer(wx.HORIZONTAL)

        # Make the labels
        field1_lbl = wx.StaticText(self, label="Field 1:")
        field2_lbl = wx.StaticText(self, label="Field 2:")

        # 1) HORIZONTAL BOXSIZERS
        field1_sz.Add(field1_lbl, 0,
                      wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5)
        field1_sz.Add(self._field1, 1, wx.EXPAND)

        field2_sz.Add(field2_lbl, 0,
                      wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5)
        field2_sz.Add(self._field2, 1, wx.EXPAND)

        # 2) VERTICAL BOXSIZER
        vsizer.AddStretchSpacer()
        BOTH_SIDES = wx.EXPAND|wx.LEFT|wx.RIGHT
        vsizer.Add(field1_sz, 0, BOTH_SIDES|wx.TOP, 50)
        vsizer.AddSpacer(15)
        vsizer.Add(field2_sz, 0, BOTH_SIDES|wx.BOTTOM, 50)
        vsizer.AddStretchSpacer()

        # Finally assign the main outer sizer to the panel
        self.SetSizer(vsizer)

它是如何工作的...

这个菜谱仅展示了我们在上一个菜谱的 __DoLayout 方法中做了哪些改动以利用这些额外的选项。在添加控件到水平尺寸器的部分,首先要注意的是我们不再有 AddSpacer 调用。这些调用已经被在 Add 调用中指定边框所取代。当我们添加每个标签时,我们添加了两个尺寸器标志,ALIGN_CENTER_VERTICALRIGHT。第一个标志是一个对齐标志,它指定了对齐的期望行为,第二个是一个边框标志,它指定了我们要将边框参数应用到哪个位置。在这种情况下,尺寸器将把 StaticText 对象对齐到垂直空间中央,并在其右侧添加 5px 的内边距。

接下来,在我们将 TextCtrl 对象添加到布局管理器时,我们指定了 1 作为比例值,并将布局管理器的标志设置为 EXPAND。将比例值设置为大于默认的 0,将告诉布局管理器为该控件在布局管理器管理区域中分配更多空间。当比例值大于 0 并与 EXPAND 标志结合时,该标志指示控件在空间可用时增大,这将允许控件在对话框大小调整到更大时进行拉伸。通常,您只需要为比例参数指定 01,但在某些复杂的布局中,可能需要为不同的控件分配相对不同数量的总可用空间。例如,在一个有两个控件的布局中,如果两个控件都被分配了比例值 1,那么它们各自将获得 50% 的空间。将其中一个控件的比例值更改为 2 将改变空间分配为 66/33 的比例平衡。

我们还使用垂直尺寸调整器对最终布局做了一些修改。首先,我们不再使用常规的AddSpacer函数来向布局中添加一些静态间隔,而是改为使用AddStretchSpacerAddStretchSpacer基本上等同于执行Add((-1,-1), 1, wx.EXPAND),这仅仅添加了一个不确定大小的间隔,当窗口大小改变时它会进行拉伸。这使得我们能够在对话框的垂直尺寸变化时保持控件位于中心。

最后,当将两个水平尺寸调整器添加到垂直尺寸调整器时,我们使用了一些标志来在尺寸调整器的LEFT, RIGHTTOPBOTTOM周围应用静态的 50px 间距。同样重要的是要注意,我们再次传递了EXPAND标志。如果我们没有这样做,垂直尺寸调整器将不允许这两个项目扩展,这反过来又会导致我们为TextCtrl对象添加的EXPAND标志失效。尝试将这个示例和上一个示例并排运行,并调整每个窗口的大小以查看行为差异。

如何工作...

之前的截图上已经画了一些线条来展示由主顶级 VERTICAL 大小调整器 vsizer 管理的五个项目。

还有更多...

有许多标志可以用来以各种方式影响布局。以下三个表格列出了这些标志的不同类别,这些类别可以在标志的位掩码中组合:

对齐标志

此表显示了所有对齐标志的列表以及每个标志的作用描述:

对齐标志 描述
wx.ALIGN_TOP 将项目对齐到可用空间顶部
wx.ALIGN_BOTTOM 将项目对齐到可用空间的底部
wx.ALIGN_LEFT 将项目对齐到可用空间的左侧
wx.ALIGN_RIGHT 将项目对齐到可用空间的右侧
wx.ALIGN_CENTER_VERTICAL wx.ALIGN_CENTRE_VERTICAL
wx.ALIGN_CENTER_HORIZONTAL wx.ALIGN_CENTRE_HORIZONTAL

边界旗帜

以下标志可用于控制 Sizer 的 Add 方法的 border 参数应用于哪个(些)控制边:

边界旗帜 描述
wx.TOP 将边框应用于项目的顶部
wx.BOTTOM 将边框应用于项目底部
wx.LEFT 应用到项目左侧的边框
wx.RIGHT 将边框应用于项目的右侧
wx.ALL 将边框应用于项目的所有边

行为标志

此表中的尺寸标志可用于控制控件在尺寸调整器内的缩放方式:

行为标志 描述
wx.EXPAND 项目将扩展以填充分配给它的空间 (wx.GROW 与之相同)
wx.SHAPED EXPAND 类似,但保持项目的宽高比
wx.FIXED_MINSIZE 不要让项目的大小小于其初始最小尺寸
wx.RESERVE_SPACE_EVEN_IF_HIDDEN 不允许调整器在项目隐藏时回收项目空间

参见

  • 请参阅本章中的使用 BoxSizer配方,了解如何使用BoxSizer的基本方法。

  • 本章中关于“使用 GridBagSizer 布局控件”的配方展示了如何使用较为复杂的布局类之一。

使用 GridBagSizer 布局控件

在 wxPython 中,除了BoxSizer之外,还有许多其他类型的sizers,它们被设计用来简化不同类型的布局。GridSizerFlexGridSizerGridBagSizer可以用来以网格状方式排列项目。GridSizer提供了一种固定网格布局,其中项目被添加到网格中的不同“单元格”中。FlexGridSizerGridSizer类似,但网格中的列可以有不同宽度。最后,GridBagSizerFlexGridSizer类似,但还允许项目跨越网格中的多个“单元格”,这使得实现通常只能通过嵌套多个BoxSizers才能实现的布局成为可能。本食谱将讨论GridBagSizer的使用,并使用它来创建一个对话框,该对话框可以用来查看日志事件的详细信息。

如何做到这一点...

在这里,我们将创建一个自定义的 DetailsDialog,它可以用于查看日志消息或系统事件。它包含两个字段,用于显示消息类型和详细的文本信息:

class DetailsDialog(wx.Dialog):
    def __init__(self, parent, type, details, title=""):
        """Create the dialog
        @param type: event type string
        @param details: long details string
        """
        super(DetailsDialog, self).__init__(parent, title=title)

        # Attributes
        self.type = wx.TextCtrl(self, value=type,
                                style=wx.TE_READONLY)
        self.details = wx.TextCtrl(self, value=details,
                                   style=wx.TE_READONLY|
                                         wx.TE_MULTILINE)

        # Layout
        self.__DoLayout()
        self.SetInitialSize()

    def __DoLayout(self):
        sizer = wx.GridBagSizer(vgap=8, hgap=8)

        type_lbl = wx.StaticText(self, label="Type:")
        detail_lbl = wx.StaticText(self, label="Details:")

        # Add the event type fields
        sizer.Add(type_lbl, (1, 1))
        sizer.Add(self.type, (1, 2), (1, 15), wx.EXPAND)

        # Add the details field
        sizer.Add(detail_lbl, (2, 1))
        sizer.Add(self.details, (2, 2), (5, 15), wx.EXPAND)

        # Add a spacer to pad out the right side
        sizer.Add((5, 5), (2, 17))
        # And another to the pad out the bottom
        sizer.Add((5, 5), (7, 0))

        self.SetSizer(sizer)

它是如何工作的...

GridBagSizerAdd 方法与其他类型的 sizer 相比,需要一些额外的参数。必须指定网格位置,并且可选地指定跨越的列数和行数。我们在详情对话框中使用了这个方法,以便在详情字段的情况下允许 TextCtrl 字段跨越多列和多行。这种布局的工作方式可能会有些复杂,所以让我们逐行查看我们的 __DoLayout 方法,看看它们是如何影响对话框布局的。

首先,我们创建了一个 GridBagSizer,并在其构造函数中指定了行与列之间想要的多余空间。接下来,我们开始向这个布局管理器中添加项目。我们添加的第一个项目是一个类型为 StaticText 的标签,它位于第 1 行第 1 列。这样做是为了在边缘周围留出一些空间。然后,我们在第 1 行第 2 列的标签右侧添加了一个 TextCtrl。对于这个项目,我们还指定了跨度参数,告诉项目跨越 1 行和 15 列。列宽是按网格中第一列的大小成比例的。

接下来我们添加详细字段,从详细标签开始,该标签添加在第 2 行第 1 列,以便与类型StaticText标签对齐。由于详细文本可能很长,我们希望它跨越多行。因此,对于其跨度参数,我们指定它跨越 5 行和 15 列。

最后,为了使底部和右侧的控制周围的填充与顶部和左侧相匹配,我们需要在右侧和底部添加一个填充空间来创建额外的列和行。请注意,对于这一步,我们需要考虑之前添加的项目所占用的跨度参数,以确保我们的项目不会重叠。项目不能占用与 sizer 中任何其他项目相同的列或行。因此,我们首先在行 2、列 17 处添加一个填充空间,以在TextCtrl对象的右侧创建一个新的列。我们指定列 17 是因为TextCtrl对象从列 2 开始,跨越 15 列。同样,我们在底部添加一个时也做了同样的事情,以考虑详细文本字段的范围。请注意,与先偏移网格中的第一个项目然后添加填充空间相比,将GridBagSizer嵌套在BoxSizer内部并指定边框会更简单。这个食谱中的方法只是为了说明在向网格添加额外项目时需要考虑项目跨度:

如何工作...

请参阅本章附带示例代码,了解使用此对话框的小型应用程序。

参见

  • 本章中关于理解比例、旗帜和边框的配方详细描述了使用尺寸标志的方法。

标准对话框按钮布局

每个平台对于如何在对话框中放置不同的对话框按钮都有自己的标准。这就是StdDialogButtonSizer发挥作用的地方。它可以用来向对话框添加标准按钮,并自动处理按钮具体位置的平台标准。本食谱展示了如何使用StdDialogButtonSizer快速轻松地向Dialog添加标准按钮。

如何做到这一点...

这里是我们自定义消息框类的代码,该类可以用作标准MessageBox的替代品,在应用程序需要在其弹出对话框中显示自定义图标的情况下使用:

class CustomMessageBox(wx.Dialog):
    def __init__(self, parent, message, title="",
                 bmp=wx.NullBitmap, style=wx.OK):
        super(CustomMessageBox, self).__init__(parent, title=title)

        # Attributes
        self._flags = style
        self._bitmap = wx.StaticBitmap(self, bitmap=bmp)
        self._msg = wx.StaticText(self, label=message)

        # Layout
        self.__DoLayout()
        self.SetInitialSize()
        self.CenterOnParent()

    def __DoLayout(self):
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)

        # Layout the bitmap and caption
        hsizer.AddSpacer(10)
        hsizer.Add(self._bitmap, 0, wx.ALIGN_CENTER_VERTICAL)
        hsizer.AddSpacer(8)
        hsizer.Add(self._msg, 0, wx.ALIGN_CENTER_VERTICAL)
        hsizer.AddSpacer(10)

        # Create the buttons specified by the style flags
        # and the StdDialogButtonSizer to manage them
        btnsizer = self.CreateButtonSizer(self._flags)

        # Finish the layout
        vsizer.AddSpacer(10)
        vsizer.Add(hsizer, 0, wx.ALIGN_CENTER_HORIZONTAL)
        vsizer.AddSpacer(8)
        vsizer.Add(btnsizer, 0, wx.EXPAND|wx.ALL, 5)

        self.SetSizer(vsizer)

它是如何工作的...

在这里,我们创建了一个自定义的 MessageBox 克隆,它可以接受一个自定义的 Bitmap 来显示,而不是仅仅使用常规 MessageBox 实现中可用的标准图标。这个类相当简单,所以让我们跳转到 __DoLayout 方法,看看我们是如何利用 StdDialogButtonSizer 的。

__DoLayout 函数中,我们首先创建了一些常规的 BoxSizers 来完成布局的主要部分,然后在一行代码中就创建了所有按钮的整个布局。为了实现这一点,我们使用了基类 wx.DialogCreateButtonSizer 方法。此方法接受一个标志掩码,用于指定要创建的按钮,然后创建它们,并将它们添加到它返回的 StdDialogButtonSizer 中。完成这些操作后,我们只需将该布局器添加到对话框的主布局器中,任务就完成了!

以下截图展示了StdDialogButtonSizer如何处理平台标准之间的差异。

例如,对话框中的确定取消按钮在 Windows 上的顺序是确定/取消

如何工作...

在 Macintosh OS X 上,按钮的标准布局是取消/确定

如何工作...

还有更多...

这里是一个快速参考,列出了可以作为掩码传递给CreateButtonSizer方法的标志,以便创建按钮管理器将管理的按钮:

标志 描述
wx.OK 创建一个确定按钮
wx.CANCEL 创建一个取消按钮
wx.YES 创建一个“是”按钮
wx.NO 创建一个“否”按钮
wx.HELP 创建一个帮助按钮
wx.NO_DEFAULT 设置“否”按钮为默认选项

参见

  • 第三章中的 创建股票按钮 菜单,用户界面基本构建块 讨论了如何从内置 ID 创建常用按钮。

  • 本章中关于使用 BoxSizer的配方讨论了使用BoxSizers进行窗口布局的基础知识。

使用 XML 资源

XRC 是一种使用 XML 资源文件创建和设计窗口布局的方法。XML 的层次结构性质与应用程序窗口层次结构相平行,这使得它成为序列化窗口布局的非常合理的数据格式。本食谱展示了如何从一个 XML 资源文件中创建和加载一个简单的对话框,该对话框上有两个 CheckBoxe 对象和两个 Button 对象。

如何做到这一点...

这里是我们存储在名为 xrcdlg.xrc: 的文件中的对话框的 XML 代码:

<?xml version="1.0" ?>
<resource>
  <object class="wxDialog" name="xrctestdlg">
    <object class="wxBoxSizer">
      <orient>wxVERTICAL</orient>
      <object class="spacer">
        <option>1</option>
        <flag>wxEXPAND</flag>
      </object>
      <object class="sizeritem">
        <object class="wxCheckBox">
          <label>CheckBox Label</label>
        </object>
        <flag>wxALL|wxALIGN_CENTRE_HORIZONTAL</flag>
        <border>5</border>
      </object>
      <object class="spacer">
        <option>1</option>
        <flag>wxEXPAND</flag>
      </object>
      <object class="sizeritem">
        <object class="wxBoxSizer">
          <object class="sizeritem">
            <object class="wxButton" name="wxID_OK">
              <label>Ok</label>
            </object>
            <flag>wxALL</flag>
            <border>5</border>
          </object>
          <object class="sizeritem">
            <object class="wxButton" name="wxID_CANCEL">
              <label>Cancel</label>
            </object>
            <flag>wxALL</flag>
            <border>5</border>
          </object>
          <orient>wxHORIZONTAL</orient>
        </object>
        <flag>wxALIGN_BOTTOM|wxALIGN_CENTRE_HORIZONTAL</flag>
        <border>5</border>
      </object>
    </object>
    <title>Xrc Test Dialog</title>
    <style>wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER</style>
  </object>
</resource>

当加载上述 XML 时,将生成以下对话框:

如何做...

这是一个最小的程序,用于加载此 XML 资源以创建并显示它所代表的对话框:

import wx
import wx.xrc as xrc
app = wx.App()
frame = wx.Frame(None)
resource = xrc.XmlResource("xrcdlg.xrc")
dlg = resource.LoadDialog(frame, "xrctestdlg")
dlg.ShowModal()
app.MainLoop()

它是如何工作的...

本食谱中的 XML 是在xrced的帮助下创建的,xrced是一个 XML 资源编辑工具,它是 wxPython 工具包的一部分。object标签用于表示类对象。通过在内部嵌套其他对象来表示 XML 中的父子关系。object标签的class属性用于指定要创建的类的类型。值应该是类名,在 wxPython 提供的类的情况下,它们使用wxWidgets的名称,这些名称以“wx”为前缀。为了与XmlResource类一起工作,强烈建议使用像xrced这样的工具来生成 XML。

为了将 XML 加载以创建用于表示的对象,您需要导入 wx.xrc 包,它提供了 XmlResource 类。有几种方法可以使用 XmlResource 对 XML 进行转换。在这个例子中,我们通过将 xrc 文件的路径传递给其构造函数来创建我们的 XmlResource 对象。此对象具有多种加载方法,用于实例化不同类型的对象。我们想要加载一个对话框,因此我们调用了它的 LoadDialog 方法,将父窗口作为第一个参数传递,然后是我们要从 XML 中加载的对话框的名称。然后它将实例化该对话框的一个实例并返回,这样我们就可以显示它了。

还有更多...

以下包含了一些在使用 XRC 库时可用功能的附加参考。

加载其他类型的资源

XmlResource 对象提供了从 XML 加载多种不同类型资源的方法。以下是其中一些附加方法的快速参考:

方法 描述
LoadBitmap(name) 加载并返回由名称标识的位图
LoadDialog(parent, name) 加载并返回名为 name 的对话框
LoadFrame(parent, name) 加载并返回由名称标识的 Frame
LoadIcon(name) 加载并返回由名称标识的图标
LoadMenu(name) 加载并返回由名称标识的菜单
LoadMenuBar(parent, name) 加载并返回由名称标识的菜单栏
LoadPanel(parent, name) 加载并返回由名称标识的 Panel
LoadToolBar(parent, name) 加载并返回由名称标识的工具栏

指定标准 ID

为了在 XRC 中给一个对象赋予一个标准的 ID,应在object标签的name属性中指定,使用wxWidgets的命名方式为 ID(即,wxID_OK,不带.)。

参见

  • 本章中的 制作自定义资源处理器 菜谱包含了一些关于使用 XRC 的额外信息。

创建自定义资源处理器

尽管 XRC 内置了对大量标准控件的支持,但任何非平凡的应用程序都将使用自己的子类和/或自定义小部件。创建一个自定义的XmlResource类将允许这些自定义类从 XML 资源文件中加载。本菜谱展示了如何创建一个用于自定义Panel类的 XML 资源处理器,然后使用该处理器来加载资源。

入门指南

本食谱讨论了如何自定义和扩展对 XML 资源的处理。请查阅本章中的使用 XML 资源食谱,以了解 XRC 工作的基础知识。

如何做到这一点...

在以下代码中,我们将展示如何为 Panel 创建一个自定义的 XML 资源处理器,然后如何使用 XRC 将该资源加载到 Frame 中:

import wx
import wx.xrc as xrc

# Xml to load our object
RESOURCE = r"""<?xml version="1.0"?>
<resource>
<object class="TextEditPanel" name="TextEdit">
</object>
</resource>
"""

在这里,在我们的 Frame 子类中,我们简单地创建了一个自定义资源处理器的实例,并使用它来加载我们的自定义 Panel:

class XrcTestFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(XrcTestFrame, self).__init__(*args, **kwargs)

        # Attributes
        resource = xrc.EmptyXmlResource()
        handler = TextEditPanelXmlHandler()
        resource.InsertHandler(handler)
        resource.LoadFromString(RESOURCE)
        self.panel = resource.LoadObject(self,
                                         "TextEdit",
                                         "TextEditPanel")

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)

这里是我们自定义资源处理器将要使用的 Panel 类。它只是一个简单的 Panel,上面有一个TextCtrl和两个Buttons

class TextEditPanel(wx.Panel):
    """Custom Panel containing a TextCtrl and Buttons
    for Copy and Paste actions.
    """
    def __init__(self, parent, *args, **kwargs):
        super(TextEditPanel, self).__init__(*args, **kwargs)

        # Attributes
        self.txt = wx.TextCtrl(self, style=wx.TE_MULTILINE)
        self.copy = wx.Button(self, wx.ID_COPY)
        self.paste = wx.Button(self, wx.ID_PASTE)

        # Layout
        self._DoLayout()

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnCopy, self.copy)
        self.Bind(wx.EVT_BUTTON, self.OnPaste, self.paste)

    def _DoLayout(self):
        """Layout the controls"""
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)

        vsizer.Add(self.txt, 1, wx.EXPAND)
        hsizer.AddStretchSpacer()
        hsizer.Add(self.copy, 0, wx.RIGHT, 5)
        hsizer.Add(self.paste)
        hsizer.AddStretchSpacer()
        vsizer.Add(hsizer, 0, wx.EXPAND|wx.ALL, 10)

        # Finally assign the main outer sizer to the panel
        self.SetSizer(vsizer)

    def OnCopy(self, event):
        self.txt.Copy()

    def OnPaste(self, event):
        self.txt.Paste()

最后,这是我们的自定义 XML 资源处理类,我们只需重写两个方法来实现对 TextEditPanel 类的处理:

class TextEditPanelXmlHandler(xrc.XmlResourceHandler):
    """Resource handler for our TextEditPanel"""
    def CanHandle(self, node):
        """Required override. Returns a bool to say
        whether or not this handler can handle the given class
        """
        return self.IsOfClass(node, "TextEditPanel")

    def DoCreateResource(self):
        """Required override to create the object"""
        panel = TextEditPanel(self.GetParentAsWindow(),
                              self.GetID(),
                              self.GetPosition(),
                              self.GetSize(),
                              self.GetStyle("style",
                                            wx.TAB_TRAVERSAL),
                              self.GetName())
        self.SetupWindow(panel)
        self.CreateChildren(panel)
        return panel

它是如何工作的...

TextEditPanel 是我们想要为其创建自定义资源处理器的自定义类。TextEditPanelXmlHandler 类是一个最小化的资源处理器,我们创建它以便能够从 XML 加载我们的类。这个类有两个必须实现的重写方法,以便它能正常工作。第一个是 CanHandle,它被框架调用以检查处理器是否可以处理给定的节点类型。我们使用了 IsOfClass 方法来检查节点是否与我们的 TextEditPanel 类型相同。第二个是 DoCreateResource,它是用来创建我们的类的。为了创建这个类,所有它的参数都可以从资源处理器中检索到。

XrcTestFrame 类是我们使用自定义资源处理器的位置。首先,我们创建了一个 EmptyXmlResource 对象,并使用它的 InsertHandler 方法将其自定义处理器添加到其中。然后,我们使用处理器的 LoadFromString 方法从我们定义的 RESOURCE 字符串中加载 XML。之后,我们只需使用资源的 LoadObject 方法加载对象即可,该方法需要三个参数:要加载的对象的 parent 窗口、XML 资源中对象的 name 以及 classname

参见

  • 请参阅第一章中的理解继承限制配方,在wxPython 入门中获取有关 wxPython 类中重写虚拟方法的一些额外信息。

  • 请参阅本章中的使用 XML 资源配方,以获取更多使用 XML 创建屏幕布局的示例。

使用 AuiFrameManager

AuiFrameManager 是 wxPython 2.8 版本中添加的先进用户界面(wx.aui)库的一部分。它允许框架拥有一个非常用户可定制的界面。它自动管理可以在窗格中解绑并转换为独立浮动窗口的子窗口。此外,还有一些内置功能,有助于在应用程序运行期间持久化和恢复窗口布局。本菜谱将创建一个具有 AUI 支持的框架基类,并在应用程序下次启动时自动保存其视角并重新加载。

如何做到这一点...

以下代码将定义一个基类,该类封装了AuiManager的一些用法:

import wx
import wx.aui as aui

class AuiBaseFrame(wx.Frame):
    """Frame base class with builtin AUI support"""
    def __init__(self, parent, *args, **kwargs):
        super(AuiBaseFrame, self).__init__(*args, **kwargs)
wx.Frame.__init__(self, parent, *args, **kwargs)

        # Attributes
        auiFlags = aui.AUI_MGR_DEFAULT
        if wx.Platform == '__WXGTK__' and \
           aui.AUI_MGR_DEFAUL & aui.AUI_MGR_TRANSPARENT_HINT:
            # Use venetian blinds style as transparent can 
            # cause crashes on Linux when desktop compositing
            # is used. (wxAUI bug in 2.8)
            auiFlags -= aui.AUI_MGR_TRANSPARENT_HINT
            auiFlags |= aui.AUI_MGR_VENETIAN_BLINDS_HINT
        self._mgr = aui.AuiManager(self, flags=auiFlags)

        # Event Handlers
        self.Bind(wx.EVT_CLOSE, self.OnAuiBaseClose)

当框架关闭时将调用 OnAuiBaseClose。我们利用这个点来获取当前窗口布局视角并将其保存,以便下次应用程序启动时使用:

def OnAuiBaseClose(self, event):
        """Save perspective on exit"""
        appName = wx.GetApp().GetAppName()
        assert appName, “No App Name Set!”
        config = wx.Config(appName)
        perspective = self._mgr.SavePerspective()
        config.Write("perspective", perspective)
        event.Skip() # Allow event to propagate

AddPane 简单地封装了对 Frame 的 AuiManager 的访问,并将给定的面板和 auiInfo 添加到其中:

    def AddPane(self, pane, auiInfo):
        """Add a panel to be managed by this Frame's
        AUI Manager.
        @param pane: wx.Window instance
        @param auiInfo: AuiInfo Object
        """
        # Delegate to AuiManager
        self._mgr.AddPane(pane, auiInfo)
        self._mgr.Update() # Refresh the layout

下一种方法是一个简单的便利方法,用于创建并添加主中心面板到管理窗口:

    def SetCenterPane(self, pane):
        """Set the main center pane of the frame.
        Convenience method for AddPane.
        @param pane: wx.Window instance
        """
        info = aui.AuiPaneInfo()
        info = info.Center().Name("CenterPane")
        info = info.Dockable(False).CaptionVisible(False)
        self._mgr.AddPane(pane, info)

这种最终方法用于从上次打开窗口时加载最后保存的窗口布局:

    def LoadDefaultPerspective(self):
        appName = wx.GetApp().GetAppName()
        assert appName, "Must set an AppName!"
        config = wx.Config(appName)
        perspective = config.Read("perspective")
        if perspective:
            self._mgr.LoadPerspective(perspective)

它是如何工作的...

在这个菜谱中,我们创建了一个类来帮助封装一些AuiManager的功能。所以,让我们来看看这个类提供的一些功能以及它是如何工作的。

__init__ 方法是我们创建将要添加到 Frame 中的 AuiManager 对象的地方。AuiManager 接受一系列可能的标志来指定其行为。我们针对使用桌面合成的 Linux 平台上的一个错误采用了一个小的解决方案。使用透明停靠提示可能导致在此场景下 AUI 应用程序崩溃,因此我们将其替换为百叶窗样式。

OnAuiBaseClose 被用作处理 Frame 关闭时的事件处理器。我们使用这个钩子来自动存储 AuiManager 的当前布局,这被称为视角,以便于下一次应用程序启动。为了实现这个功能,我们创建了一个要求,即调用 App 对象的 SetName 方法来设置应用程序名称,因为我们需要这个来使用 wx.Configwx.Config 对象是一个简单的接口,用于访问 Windows 上的注册表或在其他平台上的应用程序配置文件。SavePerspective 返回一个字符串,其中包含了 AuiManager 需要的所有信息,以便恢复当前窗口布局。然后,应用程序可以在启动时简单地调用我们的 LoadDefaultPerspective 方法,以恢复用户的最后窗口布局。

本类中的另外两种方法相当简单,主要是为了方便将任务委托给FrameAuiManagerAuiManagerAddPane方法是用来添加由它管理的面板。pane参数需要是一个Frame的子窗口对象。在实践中,这通常是一些Panel子类的实例。auiInfo参数是一个AuiPaneInfo对象。这是AuiManager用来确定如何管理面板的依据。请参阅随本食谱附带的示例代码,以了解此类在实际中的应用。

还有更多...

这里是关于可用于AuiManager标志掩码中的标志的快速参考,以便自定义其行为以及某些组件的样式:

标志 描述
AUI_MGR_DEFAULT 等同于 AUI_MGR_ALLOW_FLOATING&#124; AUI_MGR_TRANSPARENT_HINT&#124; AUI_MGR_HINT_FADE&#124; AUI_MGR_NO_VENETIAN_BLINDS_FADE
AUI_MGR_ALLOW_FLOATING 允许浮动窗格
AUI_MGR_ALLOW_ACTIVE_PANE 高亮显示当前活动窗格的标题栏
AUI_MGR_HINT_FADE 淡出视图中的停靠提示
AUI_MGR_LIVE_RESIZE 在拖动它们之间的分割条时调整窗格大小
AUI_MGR_NO_VENETIAN_BLINDS_FADE 禁用百叶窗淡入/淡出功能
AUI_MGR_RECTANGLE_HINT 显示拖动浮动窗格时的简单矩形停靠提示
AUI_MGR_TRANSPARENT_DRAG 在拖动时使浮动面板部分透明
AUI_MGR_TRANSPARENT_HINT 显示在拖动浮动面板时部分透明的浅蓝色停靠提示
AUI_MGR_VENETIAN_BLINDS_HINT 使用威尼斯百叶风格停靠提示为浮动面板

第八章. 将绘图输出到屏幕

在本章中,我们将涵盖:

  • 屏幕绘图

  • 绘制形状

  • 利用 SystemSettings

  • 使用一个GraphicsContext

  • 使用 RendererNative 绘制

  • 减少绘图过程中的闪烁

简介

能够在计算机显示上显示对象是 GUI 工具箱最基本的功能之一。在 wxPython 中,对象通过向设备上下文DC)发出的绘图命令在显示上显示。在底层,所有控件都表示为绘制在屏幕显示上的位图。设备上下文提供的接口允许自定义控件的外观。当与事件结合使用时,它们也是创建新控件的基础。

这些基础工具打开了许多门和可能性,使得应用设计师能够填补工具箱提供的空白,以满足特定应用的需求。现在工具已经介绍完毕,是时候拿起它并投入使用。

屏幕绘图

屏幕上所有可见的窗口都会向设备上下文(通常称为 DC)发出一些绘图命令,以告诉系统在屏幕上显示哪种像素信息。一些控件类,如wx.Controlwx.Windowwx.Panel,允许通过使用wx.EVT_PAINT来定义用户对屏幕上绘制内容的控制。本食谱通过创建一个简单的幻灯片小部件来介绍屏幕绘图,该部件将从目录中加载 PNG 或 JPG 文件,然后在屏幕上绘制该图像,并在其下方添加一些标签文本,以显示哪张图像是集合中的。

如何做到这一点...

在这里,我们将查看我们的ImageCanvas小部件。从其构造函数开始,我们将其BindEVT_PAINT,这样当我们的窗口的一部分被标记为需要重绘时,我们可以从框架中获取回调:

import os
import wx

class ImageCanvas(wx.PyPanel):
    def __init__(self, parent):
        super(SlideShowPanel, self).__init__(parent)

        # Attributes
        self.idx = 0 # Current index in image list
        self.images = list() # list of images found to display

        # Event Handlers
        self.Bind(wx.EVT_PAINT, self.OnPaint)

在这里,我们重写DoGetBestSize方法,以便根据显示在其中的图像大小调整小部件的大小:

    def DoGetBestSize(self):
        """Virtual override for PyPanel"""
        newsize = wx.Size(0, 0)
        if len(self.images):
            imgpath = self.images[self.idx]
            bmp = wx.Bitmap(imgpath)
            newsize = bmp.GetSize()
            newsize = newsize + (20, 20) # some padding
        else:
            tsize = self.GetTextExtent("No Image!")
            newsize = tsize + (20, 20)

        # Ensure new size is at least 300x300
        return wx.Size(max(300, newsize[0]),
                       max(300, newsize[1]))

在这里,在 OnPaint 函数中,我们处理 EVT_PAINT 事件并创建一个 PaintDC 来在面板上绘制当前图像:

    def OnPaint(self, event):
        """Draw the image on to the panel"""
        dc = wx.PaintDC(self) # Must create a PaintDC

        # Get the working rectangle
        rect = self.GetClientRect()

        # Setup the DC
        dc.SetTextForeground(wx.BLACK)

        # Do the drawing
        if len(self.images):
            # Draw the current image
            imgpath = self.images[self.idx]
            bmp = wx.Bitmap(imgpath)
            bsize = bmp.GetSize()
            # Try and center the image
            # Note: assumes image is smaller than canvas
            xpos = (rect.width - bsize[0]) / 2
            ypos = (rect.height - bsize[1]) / 2
            dc.DrawBitmap(bmp, xpos, ypos)
            # Draw a label under the image saying what
            # number in the set it is.
            imgcount = len(self.images)
            number = "%d / %d" % (self.idx+1, imgcount)
            tsize = dc.GetTextExtent(number)
            xpos = (rect.width - tsize[0]) / 2
            ypos = ypos + bsize[1] + 5 # 5px below image
            dc.DrawText(number, xpos, ypos)
        else:
            # Display that there are no images
            font = self.GetFont()
            font.SetWeight(wx.FONTWEIGHT_BOLD)
            dc.SetFont(font)
            dc.DrawLabel("No Images!", rect, wx.ALIGN_CENTER)

最后,我们添加了一些客户端代码可以与之交互的方法,以便更改图像并设置图像源目录:

def Next(self):
        """Goto next image"""
        self.idx += 1
        if self.idx >= len(self.images):
            self.idx = 0 # Go back to zero
        self.Refresh() # Causes a repaint

    def Previous(self):
        """Goto previous image"""
        self.idx -= 1
        if self.idx < 0:
            self.idx = len(self.images) - 1 # Goto end
        self.Refresh() # Causes a repaint

    def SetImageDir(self, imgpath):
        """Set the path to where the images are"""
        assert os.path.exists(imgpath)
        # Find all the images in the directory
        self.images = [ os.path.join(imgpath, img)
                        for img in os.listdir(imgpath)
                        if img.lower().endswith('.png') or
                           img.lower().endswith('.jpg') ]
        self.idx = 0

它是如何工作的...

这相当简单,所以让我们快速浏览一下,看看一切是如何工作的。首先,我们从 PyPanel 派生出了我们的 ImageCanvas 面板,这样我们就可以访问它的一些虚拟方法。接下来,在构造函数中,我们将我们的绘图处理程序 BindEVT_PAINT,这样我们就会收到 PaintEvent 通知。

下一个方法,DoGetBestSize,是一个虚拟覆盖。当框架需要我们告诉它我们的最佳尺寸时,将会调用此方法。这发生在布局计算时。我们根据当前图像的大小来确定最佳尺寸,但保留一个最小矩形区域 300x300 像素,以确保我们有足够的空间进行操作。

接下来我们来到 OnPaint。这里是本菜谱的主要焦点展开的地方。首先要注意的是,我们创建了一个 PaintDC。这是一个必须的步骤。如果在 EVT_PAINT 处理器中未创建 PaintDC,那么在像 Windows 这样的平台上刷新窗口时将会出现错误。PaintDC 提供了与 DC 的接口,这将允许我们在屏幕上绘制。

OnPaint中的大部分工作只是计算我们要绘制的内容的位置。我们通过首先获取我们必须工作的矩形来完成这个任务,这个矩形可以通过调用GetClientRect简单地返回。从这里开始,如果我们有一些图像要显示,我们会进行一些简单的计算来居中当前图像,然后使用 DC 的DrawBitmap方法将我们的Bitmap对象绘制到屏幕上。然后我们继续在图像下方绘制一些文本,以显示图像在集合中的编号。为此,我们使用GetTextExtent来获取我们的字符串将需要用当前字体绘制的屏幕大小。在没有图像的情况下,我们简单地使用带有ALIGN_CENTER标志的 DC 的DrawLabel函数在矩形的中间绘制一个警告标签。

为了方便在通过调用 SetImageDir 指定的目录中循环浏览图片,我们提供了两种方法:NextPrevious。这些方法只是简单地增加或减少我们在列表中查看的索引,然后调用 RefreshRefresh 将导致系统发出一个新的 PaintEvent。当这种情况发生时,我们的 OnPaint 处理程序将被调用,并将绘制新的图片。请参阅随此配方提供的示例代码,了解如何使用我们的 ImageCanvas 小部件构建一个示例应用程序。

参见

  • 第一章中的 使用位图 菜单,在 wxPython 入门 一书中讨论了在应用程序中使用位图的基本知识。

  • 第一章中的理解继承限制配方,使用 wxPython 入门解释了Py类的用法以及如何重写它们的虚拟方法。

绘制形状

除了能够绘制文本和位图之外,DC 组件还能够绘制任意形状和线条。这些基本工具使得创建完全定制的控件和执行诸如绘制图表等任务成为可能。本食谱通过创建一个简单的笑脸控件来探索 PaintDC 的这些附加功能。

如何做到这一点...

在这里,我们将定义从 PyControl 派生出的简单笑脸控制:

class Smiley(wx.PyControl):
    def __init__(self, parent, size=(50,50)):
        super(Smiley, self).__init__(parent,
                                     size=size,
                                     style=wx.NO_BORDER)

        # Event Handlers
        self.Bind(wx.EVT_PAINT, self.OnPaint)

在这里,OnPaint 是我们将要在 PyControl 的背景上绘制笑脸的地方:

    def OnPaint(self, event):
        """Draw the image on to the panel"""
        dc = wx.PaintDC(self) # Must create a PaintDC

        # Get the working rectangle we can draw in
        rect = self.GetClientRect()

        # Setup the DC
        dc.SetPen(wx.BLACK_PEN) # for drawing lines / borders
        yellowbrush = wx.Brush(wx.Colour(255, 255, 0))
        dc.SetBrush(yellowbrush) # Yellow fill

首先,我们将从绘制头部圆圈开始,通过找到控制矩形的中心,并使用 DrawCircle 函数绘制一个带有黑色边框的黄色圆圈,使用上面设置的当前笔刷和画笔:

        cx = (rect.width / 2) + rect.x
        cy = (rect.width / 2) + rect.y
        radius = min(rect.width, rect.height) / 2
        dc.DrawCircle(cx, cy, radius)

下一步是绘制眼睛。这个笑脸将拥有蓝色、方形的眼眸。为了做到这一点,我们首先计算眼睛的大小为整个面部面积的 1/8,然后将画笔设置为蓝色,接着使用 DC 的DrawRectangle方法绘制每只眼睛:

        eyesz = (rect.width / 8, rect.height / 8)
        eyepos = (cx / 2, cy / 2)
        dc.SetBrush(wx.BLUE_BRUSH)
        dc.DrawRectangle(eyepos[0], eyepos[1],
                         eyesz[0], eyesz[1])
        eyepos = (eyepos[0] + (cx - eyesz[0]), eyepos[1])
        dc.DrawRectangle(eyepos[0], eyepos[1],
                         eyesz[0], eyesz[1])

最后但同样重要的是在脸上画出微笑。为了做到这一点,我们将画笔颜色设置为黄色,然后使用 DC 的DrawArc方法来绘制圆的一部分。由于我们只想使用弧的下半部分作为微笑,所以我们最后画一个黄色的矩形覆盖在切片的上半部分,以遮盖掉楔形:

        dc.SetBrush(yellowbrush)
        startpos = (cx / 2, (cy / 2) + cy)
        endpos = (cx + startpos[0], startpos[1])
        dc.DrawArc(startpos[0], startpos[1],
                   endpos[0], endpos[1], cx, cy)
        dc.SetPen(wx.TRANSPARENT_PEN)
        dc.DrawRectangle(startpos[0], cy,
                         endpos[0] - startpos[0],
                         startpos[1] - cy)

它是如何工作的...

在这个菜谱中,我们使用了刷子以及PaintDC为我们提供的某些基本绘图程序。让我们来看看我们的OnPaint方法,看看一切是如何工作的。

首先,我们开始设置我们的直流绘图工具。我们设置了一支黑色Pen,当直流绘制线条时会使用它。然后我们设置了一块黄色BrushBrush用于在绘制形状时填充形状内部的区域。接下来,我们开始绘制脸部,它是一个圆形。为此,我们只需要找到我们的绘图区域中心,然后调用带有我们所需的中心点和半径的DrawCircle方法。然后直流将使用我们的PenBrush来创建一个带有黑色边框的黄色圆形。

接下来,对于眼睛,我们决定将它们绘制成蓝色方块。因此,我们切换到蓝色Brush,并调用DrawRectangle例程来绘制方块。此方法的前两个参数是矩形左上角将被绘制的位置。接下来的两个参数是矩形的宽度和高度。

最后一步是绘制微笑,这只是一个简单的弧线。为了执行这一步,我们需要确定弧线的两个端点位置,这些端点是基于我们圆的中心点来确定的。然后我们调用了DrawArc方法,这个方法会绘制圆的一部分。因为它绘制的是圆的一部分,所以会有两条从中心点延伸到弧线起始点和结束点的多余线条。为了消除这些线条,我们在它们上方绘制了一个黄色的矩形来擦除它们,只留下构成微笑的弧线。

还有更多...

这里是PaintDC的基本绘图函数的快速参考。

函数 描述
DrawArc(x1,y1,x2,y2, xcenter,ycenter) 绘制以(xcenter,ycenter)为中心,从(x1,y1)(x2,y2)的圆弧部分。
DrawBitmap(bmp,x,y, useMask=False) 在位置 x,y 绘制位图。
DrawCheckMark(x,y,width, height) 在给定的矩形中绘制勾选标记。
DrawCircle(x,y,radius) 绘制以点 x,y 为中心,给定半径的圆。
DrawEllipse(x,y,width,height) 在给定的矩形中绘制椭圆。
DrawEllipticArc(x,y,w,h, start,end) 在给定的矩形中绘制椭圆的弧线。start 和 end 参数是角度,用于指定弧线的起始和结束位置,相对于矩形中的 3 点钟位置。
DrawIcon(icon, x, y) x,y 位置绘制图标。
DrawImageLabel(lbl,bmp,rect, align) 在给定的矩形内绘制一个标签和位图,使用给定的对齐标志。
DrawLabel(text,rect,align) 在给定的对齐标志下,在矩形内绘制文本。
DrawLine(x1,y1,x2,y2) 使用当前笔从 x1,y1x2,y2 绘制一条线。
DrawPoint(x,y) 使用当前笔在 x,y 位置绘制一个点。
DrawPolygon(points,x,y) 基于点列表在位置 x,y 绘制多边形。
DrawRectangle(x,y,w,h) 在位置 x,y 绘制大小为 w,h 的矩形。
DrawRotatedText(text,x,y, angle) 在位置 x,y 以给定角度旋转绘制文本。
DrawRoundedRectangle(x,y,w,h, angle) 绘制具有圆角的矩形。
DrawSpline(points) 使用点列表绘制样条曲线。
DrawText(text,x,y) 在位置 x,y 绘制文本。

参见

  • 请参阅本章中的屏幕绘图配方,了解创建和使用DeviceContext的基本方法。

利用系统设置

SystemSettings 对象允许程序查询系统以获取有关默认颜色和字体信息。在创建自定义绘图时,能够了解这些信息非常有帮助,因为它使得使用与原生系统组件相同的颜色和字体成为可能,这样你的自定义控件或窗口装饰就可以融入其中,看起来像是与其他共享同一窗口的原生组件属于一体。在本食谱中,我们将使用 SystemSettings 创建一个类似于 StaticBox 的自定义控件,但其标题栏类似于 Frame 栏的标题栏。

如何做到这一点...

对于这个自定义控件,我们再次从 PyPanel 派生出来,以便我们可以访问其 DoGetBestSize 方法:

class CaptionBox(wx.PyPanel):
    def __init__(self, parent, caption):
        super(CaptionBox, self).__init__(parent,
                                         style=wx.NO_BORDER)

        # Attributes
        self._caption = caption
        self._csizer = wx.BoxSizer(wx.VERTICAL)

        # Setup
        self.__DoLayout()

        # Event Handlers
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    def __DoLayout(self):
        msizer = wx.BoxSizer(wx.HORIZONTAL)
        self._csizer.AddSpacer(12) # extra space for caption
        msizer.Add(self._csizer, 0, wx.EXPAND|wx.ALL, 8)
        self.SetSizer(msizer)

    def DoGetBestSize(self):
        size = super(CaptionBox, self).DoGetBestSize()

        # Compensate for wide caption labels
        tw = self.GetTextExtent(self._caption)[0]
        size.SetWidth(max(size.width, tw+20))
        return size

    def AddItem(self, item):
        """Add a window or sizer item to the CaptionBox"""
        self._csizer.Add(item, 0, wx.ALL, 5)

在这里,在我们的 EVT_PAINT 处理程序中,我们使用从 SystemSettings 单例中检索到的标题颜色,在面板顶部绘制一个简单的标题,并在其余部分绘制一个边框:

    def OnPaint(self, event):
        """Draws the Caption and border around the controls"""
        dc = wx.PaintDC(self)

        # Get the working rectangle we can draw in
        rect = self.GetClientRect()

        # Get the sytem color to draw the caption
        ss = wx.SystemSettings
        color = ss.GetColour(wx.SYS_COLOUR_ACTIVECAPTION)
        txtcolor = ss.GetColour(wx.SYS_COLOUR_CAPTIONTEXT)
        dc.SetTextForeground(txtcolor)

        # Draw the border
        rect.Inflate(-2, -2)
        dc.SetPen(wx.Pen(color))
        dc.SetBrush(wx.TRANSPARENT_BRUSH)
        dc.DrawRectangleRect(rect)

        # Add the Caption
        rect = wx.Rect(rect.x, rect.y,
                       rect.width, 16)
        dc.SetBrush(wx.Brush(color))
        dc.DrawRectangleRect(rect)
        rect.Inflate(-5, 0)
        dc.SetFont(self.GetFont())
        dc.DrawLabel(self._caption, rect, wx.ALIGN_LEFT)

它是如何工作的...

在这个示例中,我们是从 PyPanel 派生出了新的 CaptionBox 类。这样做的原因是,这个控件将成为其他控件的容器,而使用 PyPanel 将允许使用布局和尺寸管理器来管理控件的布局和尺寸。

作为__DoLayout中面板初始布局的一部分,我们在顶部预留了 20 像素的空间,在其余三边各预留了 8 像素的空间,用于标题和边框。这是通过在顶部添加一个间隔符,并在将要用于布局CaptionBox子控件BoxSizer周围额外添加 8 像素边框来实现的。同时,作为布局管理的一部分,我们重写了DoGetBestSize方法,以便处理标题文本宽度超过框子子窗口的情况。当使用这个类时,必须使用其AddItem方法来添加其子控件。

现在我们来看看如何绘制控件。在OnPaint函数中,我们首先使用SystemSettings单例来获取系统定义的标题背景和文本颜色,这将使控件能够适应并匹配在任何主题或操作系统上运行的其它控件。接下来,我们将绘制Rect向两边各缩小 2 像素来定义控件的边框。之后,我们只需将画笔设置为标题颜色并调用DrawRect来绘制边框。标题栏也是通过在布局中预留的上部空间创建一个较小的矩形,并通过将Brush设置为标题颜色来绘制一个实心矩形来类似地绘制的。最后,我们只需在刚刚绘制的矩形上绘制标题文本。请参见以下截图,它显示了两个CaptionBoxes:

工作原理...

更多内容

除了能够提供颜色外,SystemSettings 对象还可以提供系统字体和度量。三个方法 GetColourGetFontGetMetric 都接受一个索引参数,该参数是 wx.SYS_* 常量之一。

参见

  • 详见本章中关于如何创建和使用设备上下文的屏幕绘图配方。

使用 GraphicsContext

GraphicsContext 是 wxPython2.8 中的一个新特性。它提供了访问平台高级绘图功能的能力。它提供了诸如抗锯齿、浮点精度坐标系、透明度混合、渐变画笔以及一些高级方法等功能。这个示例使用它来创建一个类似于 StaticText 的自定义控件,但其背景是渐变填充的药丸形状。

如何做到这一点...

与本章中的其他配方类似,我们将从 PyControl 中派生我们的新控件,以便我们可以覆盖其 DoGetBestSize 方法来调整控件的大小以适应我们的标签:

class PodLabel(wx.PyControl):
    def __init__(self, parent, label, color):
        super(PodLabel, self).__init__(parent,
                                       style=wx.NO_BORDER)

        # Attributes
        self._label = label
        self._color = color

        # Event Handlers
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    def DoGetBestSize(self):
        txtsz = self.GetTextExtent(self._label)
        size = wx.Size(txtsz[0] + 10, txtsz[1] + 6)
        return size

这次在 OnPaint 中,我们将从我们的 PaintDC 创建一个 GCDC,并使用 GCDC 和其 GraphicsContext: 进行绘图:

    def OnPaint(self, event):
        """Draws the Caption and border around the controls"""
        dc = wx.PaintDC(self)
        gcdc = wx.GCDC(dc)
        gc = gcdc.GetGraphicsContext()

        # Get the working rectangle we can draw in
        rect = self.GetClientRect()

        # Setup the GraphicsContext
        pen = gc.CreatePen(wx.TRANSPARENT_PEN)
        gc.SetPen(pen)
        rgb = self._color.Get(False)
        alpha = self._color.Alpha() *.2 # fade to transparent
        color2 = wx.Colour(*rgb, alpha=alpha)
        x1, y1 = rect.x, rect.y
        y2 = y1 + rect.height
        gradbrush = gc.CreateLinearGradientBrush(x1, y1,
                                                 x1, y2,
                                                 self._color,
                                                 color2)
        gc.SetBrush(gradbrush)

        # Draw the background
        gc.DrawRoundedRectangle(rect.x, rect.y,
                                rect.width, rect.height,
                                rect.height/2)
        # Use the GCDC to help draw the aa text
        gcdc.DrawLabel(self._label, rect, wx.ALIGN_CENTER)

它是如何工作的...

为了在OnPaint中绘制这个控件,我们使用了PaintDC并将其包装在GCDC中。GCDC是一个内部使用GraphicsContext的设备上下文接口。使用这个接口使得在类似使用常规设备上下文的方式中使用GraphicsContext成为可能。

当设置PenBrush时,我们使用了一种透明的笔来避免在控件周围绘制边框。使用GraphicsContextCreateLinearGradientBrush方法返回的GraphicsBrush,可以轻松地用渐变色背景。此方法将创建一个从第一组坐标到第二组坐标绘制渐变的画刷,从第一种颜色开始,逐渐过渡到第二种颜色。在这种情况下,我们的第二种颜色仅在 alpha 级别上有所不同,因此渐变将淡化为半透明,这将显示出其背后的面板。

现在剩下的只是调用 GraphicsContextDrawRoundedRectangle 方法,绘制一个填充了我们之前定义的渐变的漂亮的药丸形背景。然后剩下的就是绘制背景上的标签文本。为此,我们使用了 GCDCDrawLabel 方法,它就像 PaintDCDrawLabel 方法一样,但在底层使用 GraphicsContext 来绘制平滑、抗锯齿的文本。下面的截图显示了一个示例对话框,其中包含三个 PodLabel 控件的实例。正如所见,使用 GraphicsContext 允许控件以平滑、抗锯齿的边缘和渐变背景绘制,背景在底部通过利用 GraphicsContext 的 alpha 混合渐变并变得透明。

如何工作...

参见

  • 本章中关于屏幕绘图的配方讨论了设备上下文的使用。

  • 请参阅本章中的 绘制形状 菜单以了解基本绘图例程的概述。

  • 查阅本章中的减少绘图例程中的闪烁配方,以获取更多使用GraphicsContext的示例。

使用 RendererNative 绘图

RendererNative 是一个包含一系列封装了原生 UI 组件绘制功能的类。它允许你在设备上下文中绘制诸如看起来像原生的 ButtonCheckBox 对象,而无需了解任何关于如何实现它的细节。当你需要创建通用的控件但同时又想保持平台自身控件的原生外观和感觉时,这个类非常强大且实用。本食谱使用 RendererNative 创建了一个自定义按钮类,用于显示下拉菜单。

如何做到这一点...

这个自定义按钮类将使用RendererNative根据鼠标的位置和状态来进行绘制:

class DropArrowButton(wx.PyControl):
    def __init__(self, parent, id=wx.ID_ANY,
                 label="", pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=0,
                 validator=wx.DefaultValidator,
                 name="DropArrowButton"):
        style |= wx.BORDER_NONE
        super(DropArrowButton, self).__init__(parent, id,
                                              pos, size,
                                              style,
                                              validator, name)

        # Attributes
        self._label = label
        self._menu = None
        self._state = 0

        # Event Handlers
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_LEAVE_WINDOW,
                  lambda event:
                  self.SetState(0))
        self.Bind(wx.EVT_ENTER_WINDOW,
                  lambda event:
                  self.SetState(wx.CONTROL_CURRENT))
        self.Bind(wx.EVT_PAINT, self.OnPaint)

我们重写DoGetBestSize函数,并使用标签字符串的大小作为计算按钮尺寸的基础:

    def DoGetBestSize(self):
        size = self.GetTextExtent(self._label)
        size = (size[0]+16, size[1]+16) # Drop Arrow
        size = (size[0]+8, size[1]+4) # Padding
        self.CacheBestSize(size)
        return size

在这里,我们添加了处理 EVT_LEFT_DOWNEVT_LEFT_UP 的事件处理程序,以改变控件的状态,并显示我们的弹出菜单:

    def OnLeftDown(self, event):
        """Show the drop menu"""
        self.SetState(wx.CONTROL_PRESSED)
        if self._menu:
            size = self.GetSizeTuple()
            self.PopupMenu(self._menu, (0, size[1]))

    def OnLeftUp(self, event):
        """Send a button click event"""
        if self._state != wx.CONTROL_PRESSED:
            return

        self.SetState(wx.CONTROL_CURRENT)

在这里,在 OnPaint 中,我们创建了所需的 PaintDC 并获取了对 RendererNative 单例的引用,我们将使用它来帮助我们绘制按钮的背景:

    def OnPaint(self, event):
        """Draw the Conrol"""
        dc = wx.PaintDC(self)
        gc = wx.GCDC(dc) # AA text

        # Get the renderer singleton
        render = wx.RendererNative.Get()

        # Get the working rectangle we can draw in
        rect = self.GetClientRect()

        # Draw the button
        render.DrawPushButton(self, gc, rect, self._state)
        # Draw the label on the button
        lblrect = wx.Rect(rect.x+4, rect.y+2,
                          rect.width-24, rect.height-4)
        gc.DrawLabel(self._label, lblrect, wx.ALIGN_CENTER)
        # Draw drop arrow
        droprect = wx.Rect((rect.x+rect.width)-20,
                           rect.y+2, 16, rect.height-4)
        state = self._state
        if state != wx.CONTROL_PRESSED:
            state = wx.CONTROL_CURRENT
        render.DrawDropArrow(self, gc, droprect, state)

最后,我们提供了一个 API,允许客户端代码设置按钮的弹出菜单:

    def SetMenu(self, menu):
        """Set the buttons drop menu
        @param menu: wx.Menu
        """
        if self._menu:
            self._menu.Destroy()
        self._menu = menu

    def SetState(self, state):
        self._state = state
        self.Refresh()

它是如何工作的...

在这个示例中,我们创建了一个全新的自定义按钮控件,它看起来就像一个普通的本地按钮,但有一个下拉箭头,点击时会显示一个菜单。使用RendererNative来处理大部分绘图工作极大地简化了这个看起来很棒的控件的创建过程,所以让我们看看它是如何整合在一起的。

让我们从查看 OnPaint 方法开始,因为这是控件被绘制的地方。首先,我们创建了所需的 PaintDC,然后我们使用它来创建一个 GCDC,这将允许我们绘制与原生控件一样的抗锯齿文本。然后,我们通过调用类的 Get 方法来获取 RendererNative 单例的引用。接下来,我们开始绘制控件。所有的 RenderNative 方法都接受相同的四个参数:我们正在绘制的窗口、一个 DC、一个 Rect 和渲染器标志。DrawPushButton 将使用给定的 DC 和由渲染器标志的位掩码指定的状态绘制一个原生按钮控件。在这个例子中,我们传递了三个标志之一:0 表示默认状态,CONTROL_CURRENT 表示悬停状态,CONTROL_PRESSED 表示控件被按下时。我们使用 DrawLabelDrawDropArrow 完成剩余部分,以绘制带有右侧向下箭头的按钮标签。

要使这个控件表现得像一个按钮,我们在控件的__init__方法中绑定了一系列鼠标事件。EVT_ENTER_WINDOWEVT_LEAVE_WINDOW用于通过在CONTROL_CURRENT0之间切换控件标志来切换悬停状态,EVT_LEFT_DOWN用于设置CONTROL_PRESSED状态,最后EVT_LEFT_UP用于显示弹出菜单。在每次状态改变后,调用Refresh以重新调用OnPaint处理程序并绘制控件的新状态。

还有更多...

以下列出了一些快速参考表格,其中包含了RendererNative的绘图命令以及影响其绘制控件状态的标志位。

绘图方法

以下表格是RendererNative方法的快速参考。所有方法都接受相同的四个初始参数:window, DC, rectflags

RendererNative 方法 描述
DrawCheckBox 绘制复选框
DrawChoice 绘制一个 Choice 控件
DrawComboBox 绘制一个 ComboBox
DrawComboBoxDropButton 绘制一个 ComboBox 按钮
DrawDropArrow 绘制下拉箭头
DrawHeaderButton 绘制 ListCtrl 列表头
DrawItemSelectionRect 绘制选择矩形
DrawPushButton 绘制一个 按钮
DrawRadioButton 绘制一个 RadioButton
DrawSplitterBorder 绘制 SplitterWindow 边框
DrawSplitterSash 绘制一个 SplitterWindow 分隔条
DrawTextCtrl 绘制一个 TextCtrl
DrawTreeItemButton 绘制 TreeCtrl 节点按钮

控制标志

以下标志可以作为位掩码的一部分传递给绘制方法的flags参数。不传递任何标志,或为flags参数传递0,将导致控件以默认状态绘制:

Flags 描述
CONTROL_CHECKABLE 控件可以被检查(用于 DrawCheckBox
CONTROL_CHECKED 控件被检查(用于 DrawCheckBox
CONTROL_CURRENT 鼠标悬停在控制上
CONTROL_DISABLED 控制已禁用
CONTROL_EXPANDED 仅适用于 DrawTreeItemButton
CONTROL_FOCUSED 控制拥有键盘焦点
CONTROL_ISDEFAULT 是否为默认控制(对于 DrawPushButton
CONTROL_PRESSED 按钮被按下
CONTROL_SELECTED 控制已选中
CONTROL_UNDETERMINED 复选框处于不确定状态

参见

  • 请参阅第二章中的使用鼠标食谱,响应事件部分,以获取一些使用MouseEvents的额外示例。

降低绘图过程中的闪烁

当窗口重绘导致用户界面出现可见闪烁时,就会发生闪烁现象。即使简单的绘图程序,如果操作不当,也可能引起闪烁。幸运的是,有几种方法可以用来对抗和最小化闪烁,从而改善应用程序界面的外观和感觉。本食谱展示了三种可以用来减少绘图程序中闪烁的技术片段。本章附带的示例代码包括一个示例应用程序,该应用程序使用所有这些技术来创建一个简单的动画手表控制。

如何做到这一点...

我们将从最简单的技术开始,即通过绑定到EVT_ERASE_BACKGROUND:来避免不必要的背景擦除事件。

self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)

然后在处理程序中我们不需要对它做任何事情,以防止它擦除背景:

    def OnErase(self, event):
        # Do nothing, reduces flicker by removing
        # unneeded background erasures and redraws
        pass

下一个技巧是在OnPaint处理程序中使用带缓冲的PaintDC,这样所有的单个绘图步骤都将在屏幕外执行,然后一次性将完成的产品显示在屏幕上:

    def OnPaint(self, event):
        """Draw the image on to the panel"""
        # Create a Buffered PaintDC
        dc = wx.AutoBufferedPaintDCFactory(self)

第三种技巧是在可能的情况下只重新绘制屏幕的最小部分,通过使用Refresh方法的rect参数来告诉它需要更新的窗口部分:

self Refresh(rect=RectToUpdate)

它是如何工作的...

展示的第一种技术是创建一个空的事件处理程序并将其绑定到 EVT_ERASE_BACKGROUND 事件。当你遇到绘图程序中的闪烁问题时,这通常是首先要尝试的事情。在事件处理程序中不执行任何操作,我们防止系统清除背景,这样当我们再次在 OnPaint 中绘制时,它将覆盖现有的背景。这减少了重绘的可见性,因为背景在 EVT_ERASE_BACKGROUNDEVT_PAINT 之间不会闪烁成白色。

第二种技术使用AutoBufferedPaintDCFactory来在OnPaint处理程序中创建一个缓冲的PaintDC,而不是常规的PaintDC。缓冲的 DC 在屏幕外的Bitmap上完成所有绘图,然后在一个操作中将整个新的Bitmap``Blit到屏幕上。这大大减少了闪烁,因为屏幕的更新是在一次单一的改变中完成的,而不是在直接向未缓冲的 DC 绘制屏幕时进行的许多个别改变。

展示的最后一项技术是只重新绘制需要重新绘制的屏幕最小部分。这项技术可以在控制需要由于状态变化而手动重新绘制自身的一部分时使用。例如,想象一个由一些标签文本和一张图片组成的控制。如果控制具有在鼠标悬停时更改标签颜色的行为,它可以使用rect参数调用自身的 Refresh 方法来指定控制中标签的矩形,这样只有控制的那部分被更新,最小化重新绘制屏幕的面积。

参见

  • 第二章中的“处理事件”食谱,响应事件解释了事件处理的基本原理。

  • 请参阅本章中的使用 GraphicsContext 菜谱,以获取有关使用 GraphicsContext 类绘制渐变的更详细信息。

  • 请参阅第十一章中的使用定时器配方,以获取有关使用定时器的更多信息。

第九章:设计方法和技巧

在本章中,我们将涵盖:

  • 创建单例

  • 实现观察者模式

  • 策略模式

  • 模型-视图-控制器

  • 使用混合类

  • 使用装饰器

简介

编程全在于模式。从编程语言本身,到工具集,再到应用,每个层面都有模式。能够辨别并选择最合适的解决方案来处理当前问题,有时可能是一项艰巨的任务。你了解的模式越多,你的工具箱就越丰富,选择正确工具来完成工作的难度也就越小。

不同的编程语言和工具包通常倾向于某些模式和解决问题的方法。Python 编程语言和 wxPython 也毫不例外,因此让我们深入探讨一下如何将一些常见的设计方法和技巧应用到 wxPython 应用程序中。

创建单例模式

在面向对象编程中,单例模式是一个相对简单的概念,它只允许在给定时间只有一个给定对象的实例存在。这意味着在任何给定时间,它只允许对象的一个实例存在于内存中,因此应用程序中所有对该对象的引用都是共享的。单例通常用于在应用程序中维护全局状态,因为应用程序中所有单例的实例都引用了该对象完全相同的实例。在 wxPython 核心库中,存在许多单例对象,例如ArtProviderColourDatabaseSystemSettings。本菜谱展示了如何创建一个单例Dialog类,这对于创建在给定时间只应有一个实例存在的非模态对话框非常有用,例如设置对话框或特殊工具窗口。

如何做到这一点...

要开始,我们将定义一个元类,它可以被任何需要转换为单例的类重复使用。我们将在“如何工作”部分后面详细介绍。元类是一个创建类的类。当有人尝试创建类的实例时,它会将一个类传递给它的__init____call__方法。

class Singleton(type):
    def __init__(cls, name, bases, dict):
        super(Singleton, cls).__init__(name, bases, dict)
        cls.instance = None

    def __call__(cls, *args, **kw):
        if not cls.instance:
            # Not created or has been Destroyed
            obj = super(Singleton, cls).__call__(*args, **kw)
            cls.instance = obj
            cls.instance.SetupWindow()

        return cls.instance

这里有一个使用我们元类的例子,它展示了如何通过简单地将Singleton类指定为SingletonDialog__metaclass__,轻松地将以下类转换为单例类。唯一的另一个要求是定义Singleton元类使用的SetupWindow方法,作为初始化钩子,在创建类的第一个实例时设置窗口。

注意事项

注意,在 Python 3+ 中,__metaclass__ 属性已被替换为类定义中的元类关键字参数。

class SingletonDialog(wx.Dialog):
    __metaclass__ = Singleton

    def SetupWindow(self):
        """Hook method for initializing window"""
        self.field = wx.TextCtrl(self)
        self.check = wx.CheckBox(self, label="Enable Foo")

        # Layout
        vsizer = wx.BoxSizer(wx.VERTICAL)
        label = wx.StaticText(self, label="FooBar")
        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        hsizer.AddMany([(label, 0, wx.ALIGN_CENTER_VERTICAL),
                        ((5, 5), 0),
                        (self.field, 0, wx.EXPAND)])
        btnsz = self.CreateButtonSizer(wx.OK)
        vsizer.AddMany([(hsizer, 0, wx.ALL|wx.EXPAND, 10),
                        (self.check, 0, wx.ALL, 10),
                        (btnsz, 0, wx.EXPAND|wx.ALL, 10)])
        self.SetSizer(vsizer)
        self.SetInitialSize()

它是如何工作的...

在 Python 中实现单例模式有多种方法。在这个菜谱中,我们使用元类来完成这个任务。这是一个很好地封装且易于重用的模式来完成这项任务。我们定义的 Singleton 类可以被任何定义了 SetupWindow 方法的类使用。所以,既然我们已经完成了这个任务,让我们快速看一下单例是如何工作的。

单例元类动态地为传入的类创建并添加一个名为 instance 的类变量。为了更清楚地了解这个过程,元类在我们的示例中会生成以下代码:

class SingletonDialog(wx.Dialog):
instance = None

那么第一次调用元类的 __call__ 方法时,它将分配由超类的 __call__ 方法返回的类对象实例,在这个菜谱中是一个我们的 SingletonDialog 的实例。所以基本上,它等同于以下内容:

SingletonDialog.instance = SingletonDialog(*args,**kwargs)

任何后续的初始化都将导致返回之前创建的那个,而不是创建一个新的,因为类定义维护的是对象的生存期,而不是用户代码中创建的单独引用。

我们的 SingletonDialog 类是一个非常简单的对话框,它上面有 TextCtrlCheckBoxOk Button 对象。我们不是在对话框的 __init__ 方法中调用初始化,而是定义了一个名为 SetupWindow 的接口方法,当对象最初创建时,由 Singleton 元类调用该方法。在这个方法中,我们只是简单地对对话框中的控件进行布局。如果你运行与这个主题相关的示例应用程序,你会发现无论点击显示对话框按钮多少次,它只会导致现有对话框实例被带到前台。此外,如果你在对话框的 TextCtrlCheckBox 中进行更改,然后关闭并重新打开对话框,更改将被保留,因为相同的对话框实例将被重新显示,而不是创建一个新的实例。

实现观察者模式

观察者模式是一种设计方法,其中对象可以作为其他对象发布事件的观察者进行订阅。事件的发布者(们)随后只需将事件广播给所有订阅者。这允许创建一个可扩展的、松散耦合的通知框架,因为发布者(们)不需要对观察者有任何特定的了解。由wx.lib包提供的pubsub模块通过发布者/订阅者方法提供了一个易于使用的观察者模式实现。任意数量的对象都可以将自己的回调方法订阅到发布者将发送的消息,以便进行通知。这个配方展示了如何使用pubsub模块在应用程序中发送配置通知。

如何做到这一点...

在这里,我们将创建我们的应用程序配置对象,该对象存储应用程序的运行时配置变量,并提供一个通知机制,以便在配置中添加或修改值时,通过使用观察者模式的一个接口来通知:

import wx
from wx.lib.pubsub import Publisher

# PubSub message classification
MSG_CONFIG_ROOT = ('config',)

class Configuration(object):
    """Configuration object that provides
    notifications.
    """
    def __init__(self):
        super(Configuration, self).__init__()

        # Attributes
        self._data = dict()

    def SetValue(self, key, value):
        self._data[key] = value
        # Notify all observers of config change
        Publisher.sendMessage(MSG_CONFIG_ROOT + (key,),
                              value)

    def GetValue(self, key):
        """Get a value from the configuration"""
        return self._data.get(key, None)

现在,我们将创建一个非常简单的应用程序来展示如何将观察者订阅到Configuration类中的配置更改:

class ObserverApp(wx.App):
    def OnInit(self):
        self.config = Configuration()
        self.frame = ObserverFrame(None,
                                   title="Observer Pattern")
        self.frame.Show()
        self.configdlg = ConfigDialog(self.frame,
                                      title="Config Dialog")
        self.configdlg.Show()
        return True

    def GetConfig(self):
        return self.config

此对话框将包含一个配置选项,允许用户更改应用程序的字体:

class ConfigDialog(wx.Dialog):
    """Simple setting dialog"""
    def __init__(self, parent, *args, **kwargs):
        super(ConfigDialog, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = ConfigPanel(self)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize((300, 300))

class ConfigPanel(wx.Panel):
    def __init__(self, parent):
        super(ConfigPanel, self).__init__(parent)

        # Attributes
        self.picker = wx.FontPickerCtrl(self)

        # Setup
        self.__DoLayout()

        # Event Handlers
        self.Bind(wx.EVT_FONTPICKER_CHANGED,
                  self.OnFontPicker)

    def __DoLayout(self):
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)

        vsizer.AddStretchSpacer()
        hsizer.AddStretchSpacer()
        hsizer.AddWindow(self.picker)
        hsizer.AddStretchSpacer()
        vsizer.Add(hsizer, 0, wx.EXPAND)
        vsizer.AddStretchSpacer()
        self.SetSizer(vsizer)

在此,在FontPicker的事件处理程序中,我们获取新选定的字体,并调用App对象拥有的Configuration对象的SetValue方法来更改配置,这将导致发布('config', 'font')消息:

    def OnFontPicker(self, event):
        """Event handler for the font picker control"""
        font = self.picker.GetSelectedFont()
        # Update the configuration
        config = wx.GetApp().GetConfig()
        config.SetValue('font', font)

现在,在这里,我们定义应用程序的主窗口,使其OnConfigMsg方法作为所有('config',)消息的观察者,这样每当配置被修改时,它就会被调用:

class ObserverFrame(wx.Frame):
    """Window that observes configuration messages"""
    def __init__(self, parent, *args, **kwargs):
        super(ObserverFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.txt = wx.TextCtrl(self, style=wx.TE_MULTILINE)
        self.txt.SetValue("Change the font in the config "
                          "dialog and see it update here.")

        # Observer of configuration changes
        Publisher.subscribe(self.OnConfigMsg, MSG_CONFIG_ROOT)

    def __del__(self):
        # Unsubscribe when deleted
        Publisher.unsubscribe(self.OnConfigMsg)

这里是当pubsub Publisher发送以'config'开头的任何消息时将被调用的观察者方法。在这个示例应用中,我们仅检查('config', 'font')消息,并将TextCtrl对象的字体更新为新配置的字体:

    def OnConfigMsg(self, msg):
        """Observer method for config change messages"""
        if msg.topic[-1] == 'font':
            # font has changed so update controls
            self.SetFont(msg.data)
            self.txt.SetFont(msg.data)

if __name__ == '__main__':
    app = ObserverApp(False)
    app.MainLoop()

它是如何工作的...

本食谱展示了一种通过允许应用程序感兴趣的部分在配置的某些部分被修改时订阅更新来管理应用程序配置的便捷方法。让我们快速了解一下 pubsub 是如何工作的。

Pubsub 消息使用树状结构来组织不同消息的分类。一个消息类型可以被定义为元组('root', 'child1', 'grandchild1'),或者定义为点分隔的字符串'root.child1.grandchild1'。将回调函数订阅到('root',)将会导致您的回调方法对所有以('root',)开头的消息被调用。这意味着如果一个组件发布了('root', 'child1', 'grandchild1')('root', 'child1'),那么所有订阅到('root',)的方法也将被调用。

Pubsub 基本上是通过在pubsub模块的静态内存中存储消息类型到回调的映射来工作的。在 Python 中,模块只会在你的应用程序的任何其他部分使用pubsub模块并且共享相同的单例Publisher对象时才导入一次。

在我们的配方中,Configuration 对象是一个用于存储关于我们应用程序配置数据的简单对象。它的 SetValue 方法是重要的部分,需要关注。这是每当应用程序中发生配置更改时将被调用的方法。反过来,当这个方法被调用时,它将发送一个 ('config',) + (key,) 的 pubsub 消息,这将允许任何观察者订阅根项或由确切配置项确定的更具体的主题。

接下来,我们有一个简单的 ConfigDialog 类。这只是一个简单的示例,它只提供了一个配置应用程序字体的选项。当在 ConfigPanel 中的 FontPickerCtrl 发生更改时,将从 App 中检索 Configuration 对象,并将其更新以存储新选择的 Font。当这种情况发生时,Configuration 对象将向所有已订阅的观察者发布一个更新消息。

我们的 ObserverFrame 通过将其 OnConfigMsg 方法订阅到 MSG_CONFIG_ROOT 上,成为所有 ('config',) 消息的观察者。每当调用 Configuration 对象的 SetValue 方法时,都会调用 OnConfigMsg。回调的 msg 参数将包含一个具有 topicdata 属性的 Message 对象。topic 属性将包含触发回调的消息的元组,而 data 属性将包含消息发布者与 topic 关联的任何数据。在 ('config', 'font') 消息的情况下,我们的处理程序将更新 FrameFont 和其 TextCtrl

参见

  • 请参阅本章中的 创建单例 菜谱,以了解像本菜谱中的 Publisher 这样的单例对象是如何工作的。

  • 请参阅第十章中的创建工具窗口配方,第十章,以了解创建组件和扩展功能中另一个使用发布者模式的示例。

策略模式

策略模式是一种允许应用程序在运行时选择将使用的策略或行为的途径。它通过封装不同的算法并使客户能够使用它们,而不管算法的底层行为是什么来实现这一点。这可能是编程中最基本的设计模式之一,你可能已经在不知情的情况下以某种形式使用过它。这个配方展示了如何创建一个可重用的Dialog类,该类使用策略模式来允许主要内容根据所使用的策略而变化。

如何做到这一点...

首先,我们将从定义一个包含我们对话类将使用所有策略的基本接口开始:

class BaseDialogStrategy:
    """Defines the strategy interface"""
    def GetWindowObject(self, parent):
        """Must return a Window object"""
        raise NotImplementedError, "Required method"

    def DoOnOk(self):
        """@return: bool (True to exit, False to not)"""
        return True

    def DoOnCancel(self):
        """@return: bool (True to exit, False to not)"""
        return True

现在我们来定义我们的简单“确定/取消”对话框,它将使用从我们的BaseDialogStrategy类派生出的策略,以便其主内容区域可以根据所使用的策略而变化:

class StrategyDialog(wx.Dialog):
    """Simple dialog with builtin OK/Cancel button and
    strategy based content area.   
    """
    def __init__(self, parent, strategy, *args, **kwargs):
        super(StrategyDialog, self).__init__(parent,
                                             *args,
                                             **kwargs)

        # Attributes
        self.strategy = strategy
        self.pane = self.strategy.GetWindowObject(self)

        # Layout
        self.__DoLayout()

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnButton)

在以下我们的StrategyDialog方法的实现中,我们只是委托给当前策略,以便它能够定义对话框的行为:

    def __DoLayout(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.pane, 1, wx.EXPAND)
        btnsz = self.CreateButtonSizer(wx.OK|wx.CANCEL)
        sizer.Add(btnsz, 0, wx.EXPAND|wx.ALL, 8)
        self.SetSizer(sizer)

    def GetStrategy(self):
        return self.strategy

    def OnButton(self, event):
        evt_id = event.GetId()
        bCanExit = False
        if evt_id == wx.ID_OK:
            bCanExit = self.strategy.DoOnOk()
        elif evt_id == wx.ID_OK:
            bCanExit = self.strategy.DoOnCancel()
        else:
            evt.Skip()

        if bCanExit:
            self.EndModal(evt_id)

现在我们来实现一个简单的策略,该策略可以用来让对话框显示一个用于选择文件的控件:

class FileTreeStrategy(BaseDialogStrategy):
    """File chooser strategy"""
    def GetWindowObject(self, parent):
        assert not hasattr(self, 'dirctrl')
        self.dirctrl = wx.GenericDirCtrl(parent)
        return self.dirctrl

    def DoOnOk(self):
        path = self.dirctrl.GetPath()
        if path:
            wx.MessageBox("You selected: %s" % path)
            return True
        else:
            wx.MessageBox("No file selected!")
            return False

然后,在应用中,要创建一个使用此策略的对话框,所需进行的操作如下:

# Note: ‘self’ is some window object (i.e a Frame)
strategy = FileTreeStrategy()
dlg = StrategyDialog(self, strategy, title=”Choose File”)
dlg.ShowModal()

它是如何工作的...

由于我们对话将使用的所有策略都必须可互换,因此定义一个它们将实现的接口非常重要。因此,在我们的BaseDialogStrategy类中,我们定义了一个简单的三方法接口,我们的StrategyDialog将委托给这个接口。

StrategyDialog基本上只是一个简单的通用外壳,将有关其外观和行为的所有决策委托给策略。当对话框初始化时,它会向策略请求一个窗口对象,该对象将用作对话框的主要内容区域。然后,对话框在界面中创建并添加一些标准的确定/取消按钮。

当用户点击这些按钮之一时,StrategyDialog 将简单地委托给其策略,以便策略处理用户操作。这使我们能够通过简单地实现不同的策略,以多种不同的方式重用这个对话框类。

参见

  • 请参阅本章中的模型-视图-控制器配方,以获取策略模式的一些更多示例。

模型-视图-控制器(Model View Controller)

模型-视图-控制器MVC)是一种设计模式,它在一个程序的架构中创建了一个清晰的关注点分离。它分为三个层次:底层的模型,其中包含应用程序的数据对象和业务逻辑,顶层的视图,通常由用于显示和编辑数据的控件组成,最后是中间的控制器,它负责在模型和视图之间以及反之亦然的数据流的中介:

模型-视图-控制器

MVC 实际上是由其他更简单的模式组合而成的一个大怪物模式。模型(Model)实现了一个观察者模式(observer pattern),以便在变化时更新感兴趣的各方,这使得它可以从视图(View)和控制器(Controller)中独立实现。另一方面,视图(View)和控制器(Controller)实现了一个策略模式(strategy pattern),其中控制器(Controller)是一个实现视图行为的策略。

在这个菜谱中,我们探讨如何创建一个简单的数字生成器应用程序,该程序在 wxPython 中实现了这个模式。

如何做到这一点...

由于存在多个需要协同工作的组件,定义接口是这个过程的重要一步,因此首先让我们定义一些基类,这些基类将定义我们数字生成器模型和控制器接口。

从我们模型的界面开始,我们提供了一个类,该类只需重写其Generate方法即可提供特定实现的行怍。我们还内置了一个简单的观察者模式机制,允许视图订阅模型中的更新通知:

class ModelInterface(object):
    """Defines an interface for a simple value
    generator model.
    """
    def __init__(self):
        super(ModelInterface, self).__init__()

        # Attributes
        self.val = 0
        self.observers = list()

    def Generate(self):
        """Interface method to be implemented by
        subclasses.
        """
        raise NotImplementedError

    def SetValue(self, val):
        self.val = val
        self.NotifyObservers()

    def GetValue(self):
        return self.val

    def RegisterObserver(self, callback):
        """Register an observer callback
        @param: callable(newval)
        """
        self.observers.append(callback)

    def NotifyObservers(self):
        """Notify all observers of current value"""
        for observer in self.observers:
            observer()

接下来,我们有框架视图控制器的基本接口定义,我们的控制器需要从中继承。这仅仅定义了一个简单的DoGenerateNext方法,该方法必须由具体的实现类重写:

class ControllerInterface(object):
    """Defines an interface a value generator
    controller.
    """
    def __init__(self, model):
        super(ControllerInterface, self).__init__()

        # Attributes
        self.model = model
        self.view = TheView(None, self, self.model,
                            "Fibonacci Generator")

        # Setup
        self.view.Show()

    def DoGenerateNext(self):
        """User action request next value"""
        raise NotImplementedError

现在我们来定义一些实现接口并提供特殊化的子类。

从我们的FibonacciModel类开始,我们定义了一个将生成斐波那契数的模型:

class FibonacciModel(ModelInterface):
    def Generate(self):
        cval = self.GetValue()
        # Get the next one
        for fib in self.fibonacci():
            if fib > cval:
                self.SetValue(fib)
                break

    @staticmethod
    def fibonacci():
        """Fibonacci generator method"""
        a, b = 0, 1
        while True1:
            yield a
            a, b = b, a + b

然后,我们的 FibonacciController 提供了控制器特殊化,在这个例子中,它只是对用户界面进行了一次更新,即在模型计算下一个值时禁用按钮:

class FibonacciController(ControllerInterface):
    def DoGenerateNext(self):
        self.view.EnableButton(False)
        self.model.Generate()

现在模型和控制器已经定义好了,让我们来看看我们的视图,它由一个Frame、一个包含用于显示模型中存储的当前值的TextCtrlPanel以及一个用于检索模型定义的序列中下一个值的Button组成:

class TheView(wx.Frame):
    def __init__(self, parent, controller, model, title):
        """The view for """
        super(TheView, self).__init__(parent, title=title)

        # Attributes
        self.panel = ViewPanel(self, controller, model)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize((300, 300))

    def EnableButton(self, enable=True):
        self.panel.button.Enable(enable)

在这里,ViewPanel 是我们与模型和控制器交互的地方。我们在初始化时从模型中检索初始值,然后注册为模型变化的观察者:

class ViewPanel(wx.Panel):
    def __init__(self, parent, controller, model):
        super(ViewPanel, self).__init__(parent)

        # Attributes
        self.model = model
        self.controller = controller
        initial = str(self.model.GetValue())
        self.text = wx.TextCtrl(self, value=initial)
        self.button = wx.Button(self, label="Generate")

        # Layout
        self.__DoLayout()

        # Setup
        self.model.RegisterObserver(self.OnModelUpdate)

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnAction)

    def __DoLayout(self):
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)

        vsizer.AddStretchSpacer()
        vsizer.Add(self.text, 0, wx.ALIGN_CENTER|wx.ALL, 8)
        hsizer.AddStretchSpacer()
        hsizer.AddWindow(self.button)
        hsizer.AddStretchSpacer()
        vsizer.Add(hsizer, 0, wx.EXPAND)
        vsizer.AddStretchSpacer()
        self.SetSizer(vsizer)

这里是我们的观察者方法,当模型更新为新值时将会被调用:

    def OnModelUpdate(self):
        """Observer method"""
        value = self.model.GetValue()
        self.text.SetValue(str(value))
        self.button.Enable(True)

此事件处理器用于按钮,并且将其委托给控制器,以便允许控制器执行特定实现的动作:

    def OnAction(self, event):
        self.controller.DoGenerateNext()

最后,我们将所有内容整合并实现一个应用程序:

class ModelViewApp(wx.App):
    def OnInit(self):
        self.model = FibonacciModel()
        self.controller = FibonacciController(self.model)
        return True

if __name__ == '__main__':
    app = ModelViewApp(False)
    app.MainLoop()

它是如何工作的...

使用 MVC 设计应用程序框架需要相当多的自律。正如这个简单的例子所示,需要做很多额外的“事情”。正如之前所描述的,MVC 将关注点分为三个主要角色:

  1. 模型

  2. 视图

  3. 控制器

因此,让我们看看这些角色是如何在我们的示例食谱中结合在一起的。

首先,是模型:它具有存储值和在其Generate方法被调用时生成序列中下一个值的能力。在这个菜谱中,我们实现了一个模型,用于计算和存储斐波那契数。从这个例子中要吸取的重要部分是,模型对视图或控制器没有任何直接的知识。

接下来让我们跳转到视图部分,它仅仅显示一个TextCtrl字段和一个Button。它并不了解控制器或模型如何工作的任何细节。它只通过定义好的接口与它们进行交互。当用户点击Button时,它会请求控制器决定要做什么。为了知道模型何时发生变化,它会在模型上注册一个回调函数,作为观察者来监听模型SetValue方法的调用。

现在转到控制器,它是将模型和视图粘合在一起的中介。控制器主要负责根据模型的状态实现视图的行为。我们这个菜谱的简单控制器只有一个接口方法,这个方法是在视图中的“按钮”点击事件响应时被调用的。这个动作首先禁用“按钮”,然后告诉模型生成序列中的下一个数字。

还有更多...

你可能想知道“这一切额外的繁琐有什么意义?”创建这样一个简单应用。嗯,由于模型(Model)与视图(View)完全分离,它可以在自动测试套件中更容易地进行单元测试。除此之外,由于视图(View)仅仅是一个视图,并不实现任何行为,因此如果,例如,我们想要向我们的应用中添加一个素数生成器模型,它就可以很容易地被重用。

可维护性也得到了提升,因为这三个部分是分离的,可以单独工作而不会干扰其他组件。由于这些优点,许多其他工具包,如 Django 和 web2py,都采用了这种模式。

参见

  • 请参阅本章中的 实现观察者模式 菜单,了解另一种使用观察者模式的方法。

  • 有关使用策略的更多信息,请参阅本章中的策略模式配方。

使用混合类

混合类是一种类似于策略模式的设计方法,但它直接使用继承来向新类添加扩展/通用功能。本食谱展示了如何创建一个混合类,该类可以为使用它的任何类添加调试日志功能。

如何做到这一点...

首先,让我们创建我们的LoggerMixin类,它将为需要日志功能的类提供日志功能。它简单地提供了一个Log方法,该方法将传入的字符串写入文件:

import os
import time
import wx

class LoggerMixin:
    def __init__(self, logfile="log.txt"):
        """@keyword logfile: path to log output file"""
        # Attributes
        self.logfile = logfile

    def Log(self, msg):
        """Write a message to the log.
        Automatically adds timestamp and originating class
        information.
        """
        if self.logfile is None:
            return

        # Open and write to the log file
        with open(self.logfile, 'ab') as handle:
            # Make a time stamp
            ltime = time.localtime(time.time())
            tstamp = "%s:%s:%s" % (str(ltime[3]).zfill(2),
                                   str(ltime[4]).zfill(2),
                                   str(ltime[5]).zfill(2))
            # Add client info
            client = getattr(self, 'GetName',
                             lambda: "unknown")()
            # Construct the final message
            output = "[%s][%s] %s%s" % (tstamp, client,
                                        msg, os.linesep)
            handle.write(output)

然后,要在应用程序中使用LoggerMixin,只需将其混合到任何类中即可为其添加Log方法:

class MixinRecipeFrame(wx.Frame, LoggerMixin):
    """Main application window"""
    def __init__(self, parent, *args, **kwargs):
        wx.Frame.__init__(self, parent, *args, **kwargs)
        LoggerMixin.__init__(self)
        self.Log("Creating instance...")

        # Attributes
        self.panel = MixinRecipePanel(self)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize((300, 300))

它是如何工作的...

本食谱中的混合类是LoggerMixin类。它将为使用它的类添加一个Log方法,该方法将接受一个简单的字符串作为参数,并将其写入指定的日志文件,同时包含一个时间戳和一个 ID,以显示消息的来源。

混入(mixin)通过使用多重继承来为类添加额外的功能。LoggerMixin 混入类可以与任何 Python 类一起使用,但它期望(但不要求)被混合的类有一个 GetName 方法,用于获取日志消息的 ID 部分:

[17:42:24][unknown] OnInit called
[17:42:24][frame] Creating instance...
[17:42:24][panel] Begin Layout
[17:42:24][panel] End Layout
[17:42:26][panel] Button -203: Clicked
[17:42:26][panel] Button -203: Clicked 

还有更多

wx.lib.mixins 包提供了一系列实用的混合类。以下是一些可用的混合类及其提供的功能简介。

ListCtrl 混合

以下所有混合类都是为了与ListCtrl子类一起使用,并由wx.lib.mixins.listctrl模块提供:

混合类 描述
CheckListCtrlMixin ListCtrl 的第一列添加 CheckBox 功能
ColumnSorterMixin 处理在点击ListCtrl的列标题时对项目进行排序
ListCtrlAutoWidthMixin 自动调整 ListCtrl 的最后一列大小以填充任何剩余空间
ListRowHighlighter 自动在 ListCtrl 的交替行上更改背景颜色,以使其呈现条纹外观
TextEditMixin ListCtrl 的任何列添加显示可编辑文本字段的能力

TreeCtrl 混合

以下所有混合类都是用于与TreeCtrl子类一起使用,并由wx.lib.mixins.treectrl模块提供:

混合类 描述
DragAndDrop 帮助简化向 TreeCtrl 添加拖放支持
ExpansionState 用于在 TreeCtrl 中保存和恢复节点展开状态的辅助工具
VirtualTree 允许 TreeCtrl 被虚拟化,以便按需添加和删除节点,而不是必须在前端构建整个树结构

使用装饰器

由于窗口层次结构,可能会向程序员展示一些架构问题,这些问题会导致一些繁琐且不必要的代码重复,因为需要在包含层次结构的每一级都拥有代理访问器方法或属性。通常,应用程序中的任何框架或对话框都按照以下图示的结构组织:

使用装饰器

当需要检索或修改窗口中显示的数据时,需要访问的是小部件和控件。这些控件包含在面板中,而面板又包含在顶级窗口中。由于面板负责其子控件,它通常会提供修改和访问其子控件维护的数据的方法。因此,顶级窗口类通常需要具有重复的方法,这些方法只是委托给面板的方法来获取和设置窗口的数据。这些委托方法之所以需要,是因为顶级窗口是在应用程序级别实例化的对象,应用程序在使用它时不应需要知道顶级窗口的Panel的详细信息。

本食谱展示了如何创建一个简单的装饰器方法,该方法利用 Python 的动态特性,以便将自定义面板类的一个选择方法暴露给其顶级窗口容器。

如何做到这一点...

这个装饰器类接受一个类的名称作为参数,并将动态地在顶级窗口的目标子Panel中定义delegate方法:

class expose(object):
    """Expose a panels method to a to a specified class
    The panel that is having its method exposed by this
    decorator must be a child of the class its exposing
    itself too.
    """
    def __init__(self, cls):
        """@param cls: class to expose the method to"""
        super(expose, self).__init__()
        self.cls = cls

这里发生了魔法。我们使用 setattr 动态地向目标类添加一个与被装饰函数同名的新函数。当从目标类调用时,新方法将遍历窗口的子元素以找到其 Panel,并将调用委托给子类的函数:

    def __call__(self, funct):
        """Dynamically bind and expose the function
        to the toplevel window class.
        """
        fname = funct.func_name
        def delegate(*args, **kwargs):
            """Delegate method for panel"""
            self = args[0] # The TLW
            # Find the panel this method belongs to
            panel = None
            for child in self.GetChildren():
                if isinstance(child, wx.Panel) and \
                   hasattr(child, fname):
                    panel = child
                    break
            assert panel is not None, "No matching child!"
            # Call the panels method
            return getattr(panel, fname)(*args[1:], **kwargs)

        # Bind the new delegate method to the tlw class
        delegate.__name__ = funct.__name__
        delegate.__doc__ = funct.__doc__
        setattr(self.cls, fname, delegate)

        # Return original function to the current class
        return funct

本章节附带的示例代码包含一个示例应用程序,展示了如何使用这个装饰器。

它是如何工作的...

这个配方与其说是一个设计模式,不如说是一种帮助快速编写新的 DialogFrame 类以及减少代码重复的技术。为了做到这一点,我们创建了一个装饰器类,用于将子 Panel 类的方法暴露给它们的父级顶级窗口。让我们先看看 expose 装饰器,看看它是如何施展魔法的。

expose 装饰器接受一个单一参数,即方法应该暴露给该类。在构造函数中保存对这个参数的引用,以便在装饰器应用其 __call__ 方法时后续使用。__call__ 方法创建一个名为 delegate 的方法,它将搜索第一个具有与正在暴露的方法相同名称的子面板。如果找到合适的面板,它将简单地调用面板的方法并返回其值。接下来,它使用 setattr 将新创建的 delegate 方法(具有与 Panel 的方法匹配的别名)插入到装饰器构造函数中指定的类的命名空间中。此时,该方法即可在调用 expose 时使用的顶层窗口中使用。最后,我们只需将未修改的原始函数返回给它所属的 Panel 类。

为了明确起见,这个装饰器,正如在这个菜谱中定义的那样,只能由具有已知关系的Panel子类使用,即它们是它们父窗口的唯一子窗口。这通常是大多数FrameDialog子类构建的方式,正如在本章示例代码中包含的CommentDialog类所示。

参见

  • 请参阅第一章中的理解窗口层次结构配方,以获取对不同对象包含层次结构的额外解释。

第十章:创建组件和扩展功能

在本章中,我们将涵盖:

  • 自定义 ArtProvider

  • StatusBar添加控件

  • 创建一个工具窗口

  • 创建一个 搜索栏

  • 使用 ListCtrl 混合

  • StyledTextCtrl 自定义高亮

  • 创建自定义控件

简介

一旦您使用 wxPython 了一段时间,您可能会发现您需要一些默认情况下普通控件所不具备的功能或行为。因此,为了获得这些功能,您可能需要一定程度的定制,甚至可能需要创建一种全新的控件类型,以便提供您应用程序和用户所需的用户界面。

许多控件内置了相当大的灵活性,可以通过使用它们的样式标志来改变它们的行为。然而,在本章中,我们将探讨一些面向对象的方法来创建新的控件,以及通过继承扩展一些标准控件的功能。那么,让我们开始并跳入一些食谱吧。

自定义 ArtProvider

ArtProvider 是一个单例对象,可以被任何想要显示系统主题提供的位图的组件使用。在 wxPython 2.8 中,只有 GTK(Linux)端口有这个对象的本地实现,因此其他平台使用的是内置到 wxPython 中的图标。这些内置图标至少看起来有些过时和不太合适。这个菜谱展示了如何创建一个自定义的 ArtProvider 来处理 Windows 和 OS X 上自定义图标的显示,同时仍然保留 Linux 上的本地系统主题图标。

如何做到这一点...

在这里,我们定义我们的自定义 ArtProvider 实现,这只需要我们重写 CreateBitmap 方法,该方法用于加载我们的自定义图标:

class TangoArtProvider(wx.ArtProvider):
    def __init__(self):
        super(TangoArtProvider, self).__init__()

        # Attributes
        self.bmps = [bmp.replace('.png', '')
                     for bmp in os.listdir('tango')
                     if bmp.endswith('.png')]

    def CreateBitmap(self, id,
                     client=wx.ART_OTHER,
                     size=wx.DefaultSize):

        # Return NullBitmap on GTK to allow
        # the default artprovider to get the
        # system theme bitmap.
        if wx.Platform == '__WXGTK__':
            return wx.NullBitmap

        # Non GTK Platform get custom resource
        # when one is available.
        bmp = wx.NullBitmap
        if client == wx.ART_MENU or size == (16,16):
            if id in self.bmps:
                path = os.path.join('tango', id+'.png')
                bmp = wx.Bitmap(path)
        else:
            # TODO add support for other bitmap sizes
            pass

        return bmp

然后我们只需要在应用程序中使用自定义的 TangoArtProvider,将其推送到 ArtProvider 栈中:

class ArtProviderApp(wx.App):
    def OnInit(self):
        # Push our custom ArtProvider on to
        # the provider stack.
        wx.ArtProvider.PushProvider(TangoArtProvider())
        title = “Tango ArtProvider"
        self.frame = ArtProviderFrame(None,
                                      title=title)
        self.frame.Show()
        return True

它是如何工作的...

ArtProvider 单例维护了一个由 ArtProvider 对象组成的栈,这些对象是相互链接的。当在 ArtProvider 上调用 GetBitmap 方法时,它将首先向栈顶的对象请求所需的 Bitmap。如果该对象返回 NullBitmap,它将请求下一个对象,以此类推,直到找到 Bitmap 或到达栈底。

创建自定义的 ArtProvider 所需做的所有事情就是创建一个覆盖 CreateBitmap 方法的子类。我们的 TangoArtProvider 覆盖了这个方法,并从免费的 Tango (tango.freedesktop.org) 图标集中提供了一小套图标。我们仅仅有一个包含一些 PNG 图像的文件夹,我们将这些图像映射到一些 wxPython 的 ART_* ID 上,然后在需要时从磁盘加载它们到 Bitmap 中。

参见

  • 请参阅第九章中的创建单例配方,设计方法和技巧,以了解单例(如ArtProvider)的解释。

向状态栏添加控件

StatusBar 是许多应用程序中常见的一个组件,用于在主窗口内容区域的底部显示简短的信息消息。标准的 StatusBar 支持显示多个状态文本字段。本食谱展示了如何创建一个高级的 StatusBar,其中内置了一个 Gauge 以便在长时间运行的任务中显示进度。为了提前一睹我们将要创建的内容,请查看以下截图以查看 ProgressStatusBar 的实际应用:

将控件添加到状态栏

如何做到这一点...

首先,我们将通过创建StatusBar的子类来创建我们的ProgressStatusBar类。在构造函数中,我们创建一个用于显示进度的Gauge和一个用于更新GaugeTimer

class ProgressStatusBar(wx.StatusBar):
    """Custom StatusBar with a built-in progress bar"""
    def __init__(self, parent, id_=wx.ID_ANY,
                 style=wx.SB_FLAT,
                 name="ProgressStatusBar"):
        super(ProgressStatusBar, self).__init__(parent,
                                                id_,
                                                style,
                                                name)

        # Attributes
        self._changed = False   # position has changed ?
        self.busy = False       # Bar in busy mode ?
        self.timer = wx.Timer(self)
        self.prog = wx.Gauge(self, style=wx.GA_HORIZONTAL)
        self.prog.Hide() # Hide on init

        # Layout
        self.SetFieldsCount(2)
        self.SetStatusWidths([-1, 155])

        # Event Handlers
        self.Bind(wx.EVT_IDLE, 
                  lambda evt: self.__Reposition())
        self.Bind(wx.EVT_TIMER, self.OnTimer)
        self.Bind(wx.EVT_SIZE, self.OnSize)

    def __del__(self):
        if self.timer.IsRunning():
            self.timer.Stop()

以下辅助方法用于确保当Gauge控制的位置或大小发生变化时,它会被重新定位到最右侧的状态字段:

    def __Reposition(self):
        """Repositions the gauge as necessary"""
        if self._changed:
            lfield = self.GetFieldsCount() - 1
            rect = self.GetFieldRect(lfield)
            prog_pos = (rect.x + 2, rect.y + 2)
            self.prog.SetPosition(prog_pos)
            prog_size = (rect.width - 8, rect.height - 4)
            self.prog.SetSize(prog_size)
        self._changed = False

    def OnSize(self, evt):
        self._changed = True
        self.__Reposition()
        evt.Skip()

Timer事件处理器用于处理在不确定模式下使用Gauge时进行脉冲的Gauge:

    def OnTimer(self, evt):
        if not self.prog.IsShown():
            self.Stop()

        if self.busy:
            # In busy (indeterminate) mode
            self.prog.Pulse()

从这里的 Run 方法开始,我们为用户代码操作 StatusBar 的 Gauge 添加了一些公共方法。

    def Run(self, rate=100):
        if not self.timer.IsRunning():
            self.timer.Start(rate)

    def GetProgress(self):
        return self.prog.GetValue()

    def SetProgress(self, val):
        if not self.prog.IsShown():
            self.ShowProgress(True)

        # Check if we are finished
        if val == self.prog.GetRange():
            self.prog.SetValue(0)
            self.ShowProgress(False)
        else:
            self.prog.SetValue(val)

    def SetRange(self, val):
        if val != self.prog.GetRange():
            self.prog.SetRange(val)

    def ShowProgress(self, show=True):
        self.__Reposition()
        self.prog.Show(show)

    def StartBusy(self, rate=100):
        self.busy = True
        self.__Reposition()
        self.ShowProgress(True)
        if not self.timer.IsRunning():
            self.timer.Start(rate)

    def StopBusy(self):
        self.timer.Stop()
        self.ShowProgress(False)
        self.prog.SetValue(0)   # Reset progress value
        self.busy = False

    def IsBusy(self):
        """Is the gauge busy?"""
        return self.busy

请参阅本章附带的示例代码,以了解ProgressStatusBar的实际应用示例。

它是如何工作的...

这道菜谱的主要技巧是需要手动维护Gauge控制的大小和位置,以确保它在StatusBar上保持相同的相对位置,无论窗口如何移动或调整大小。我们通过我们的__Reposition方法来处理这个问题,该方法简单地根据StatusBar中最右侧的字段来定位和调整Gauge的大小。然后,我们只需在隐藏或显示 Gauge、窗口调整大小或必要时在OnIdle期间调用此方法即可。

ProgressStatusBar 类支持进度条两种操作模式。Gauge 可以以忙碌模式(不确定)或增量模式显示。在忙碌模式下,我们只需在事件处理程序中启动并运行一个 TimerPulse 进度条。在增量模式下,首先使用 SetRange 设置 Gauge 的范围,然后根据需要通过调用 SetProgress 逐步更新其进度。

参见

  • 请参阅第五章,提供信息和提醒用户中的创建自定义启动画面配方,以了解使用仪表控件和计时器的另一个示例。

创建工具窗口

ToolWindow 是一个小型的浮动窗口,通常像 ToolBar 一样工作,在其上有很多不同的工具图标,可以通过点击来启动各种操作。这类窗口通常在绘画应用程序中用于存放调色板和其他工具。本食谱展示了如何创建一个简单的 ToolWindow 类。

如何做到这一点...

首先,让我们通过从 MiniFrame 派生来定义基本的 ToolWindow 类,使其成为一个小型、浮动、顶级窗口:

import wx
import wx.lib.pubsub as pubsub

# message data will be tool id
MSG_TOOL_CLICKED = ('toolwin', 'clicked')

class ToolWindow(wx.MiniFrame):
    def __init__(self, parent, rows=1, columns=0, title=''):
        style = wx.CAPTION|wx.SYSTEM_MENU|\
                wx.SIMPLE_BORDER|wx.CLOSE_BOX
        super(ToolWindow, self).__init__(parent,
                                         title=title,
                                         style=style)

        # Attributes
        self.panel = ToolPanel(self, rows, columns)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)

    def AddTool(self, id, bmp, helplbl=''):
        """Add a tool to the window"""
        self.panel.AddTool(id, bmp, helplbl)
        self.Fit()

ToolPanel 类充当添加到 ToolWindow: 的工具的容器和管理者。

class ToolPanel(wx.Panel):
    """Panel to hold the tools"""
    def __init__(self, parent, rows, columns):
        super(ToolPanel, self).__init__(parent)

        # Attributes
        self.sizer = wx.FlexGridSizer(rows, columns, 5, 5)

        # Setup
        self.SetSizer(self.sizer)

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnButton)

AddTool 方法接收传入的 ID 和位图,创建一个 BitmapButton 作为工具,然后简单地将它添加到布局管理器的布局中:

    def AddTool(self, id, bmp, helplbl=''):
        tool = wx.BitmapButton(self, id, bmp)
        tool.SetToolTipString(helplbl)
        self.sizer.Add(tool)
        self.Layout()

OnButton 处理器捕获 Panel 中的所有按钮点击,然后向所有已订阅工具消息的观察者发布通知:

    def OnButton(self, event):
        """Notify clients when tool is clicked"""
        pubsub.Publisher.sendMessage(MSG_TOOL_CLICKED,
                                     event.GetId())

请参阅本章附带示例代码,以了解使用ToolWindow的文本编辑器应用程序的示例。

它是如何工作的...

现在我们已经看到了代码,让我们快速浏览一下,以便我们可以看到它是如何协同工作的。

我们的ToolWindow类由一个MiniFrame和一个Panel组成,当客户端代码调用其AddTool方法时,将会向其添加BitmapButtonsToolWindow有两个参数,rowscolumns,可以指定在ToolPanelFlexGridSizer中布局工具时使用的尺寸。为了确保ToolWindow的大小正确并且所有工具都可见,有必要在ToolPanelAddTool方法中调用Layout,然后对ToolWindow调用Fit以确保布局重新计算并且窗口大小调整以最佳适应其内容。

当在 ToolWindow 上点击一个工具时,按钮的事件处理程序简单地使用 pubsub 发送一个包含工具 ID 的消息到 MSG_TOOL_CLICKED 主题的任何观察者。选择这种通知方式是因为这样,如果应用程序有多个窗口,它们都可以共享同一个 ToolWindow,而不是每个窗口都创建它自己的实例。

参见

  • 请参阅第九章中的实现观察者模式配方,在设计方法和技巧中深入讨论使用观察者模式。

创建一个搜索栏

搜索栏已成为许多应用中一个相当熟悉的组件,作为替代显示可以覆盖部分屏幕、遮挡搜索区域的FindDialog。wxPython 中没有内置的控制来实现这一功能,因此这个菜谱展示了如何创建一个简单的SearchBar控制。

如何做到这一点...

我们的 SearchBar 控件将是一个由 Panel 作为基础,并在其上添加 SearchCtrl 以允许输入搜索文本的复合控件:

class SearchBar(wx.Panel):
    def __init__(self, parent):
        style = wx.BORDER_RAISED
        super(SearchBar, self).__init__(parent,
                                        style=style)

        # Attributes
        self.search = wx.SearchCtrl(self,
                                    size=(250, -1),
                                    style=wx.TE_PROCESS_ENTER)
        self.lastfind = ''

        # Layout
        self.__DoLayout()

        # Event Handlers
        if wx.Platform in ['__WXMSW__', '__WXGTK__']:
            # Workaround for composite control on msw/gtk
            for child in self.search.GetChildren():
                if isinstance(child, wx.TextCtrl):
                    child.Bind(wx.EVT_KEY_UP, self.OnEnter)
                    break
        else:
            self.search.Bind(wx.EVT_KEY_UP, self.OnEnter)
        self.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnCancel)

    def __DoLayout(self):
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.search, 0, wx.ALL, 2)
        self.SetSizer(sizer)

在这里,在OnCancel中,我们处理SearchCtrl的取消按钮事件,以便清除当前搜索文本并隐藏取消按钮:

    def OnCancel(self, event):
        self.search.SetValue("")
        self.search.ShowCancelButton(False)

OnEnter 将处理由 SearchCtrl 生成的键盘事件。我们使用它来查看用户何时按下回车键以启动搜索。我们通过创建一个 FindDialogEvent 来允许客户端绑定到 EVT_FIND 并处理搜索:

    def OnEnter(self, event):
        """Send a search event"""
        code = event.GetKeyCode()
        val = self.search.GetValue()
        if code == wx.WXK_RETURN and val:
            if val == self.lastfind:
                etype = wx.wxEVT_COMMAND_FIND
            else:
                etype = wx.wxEVT_COMMAND_FIND_NEXT
            fevent = wx.FindDialogEvent(etype)
            fevent.SetFindString(val)
            self.ProcessEvent(fevent)
            self.lastfind = val
        else:
            show = bool(val)
            self.search.ShowCancelButton(show)

运行本食谱所附带的示例应用程序,将显示如下窗口:

如何做...

它是如何工作的...

这个菜谱展示了如何制作一个非常基本的复合控件。SearchBar只是一个简单的Panel,上面有一个SearchCtrl。要在Frame中使用它,只需创建一个垂直的BoxSizer并将SearchBar添加到其中,这样它就会位于搜索将进行的主体内容区域下方或上方。然后,Frame可以响应SearchBar发出的事件。为了支持在用户在SearchCtrl中按下Return键时发送查找事件,我们不得不做一些事情。现在,让我们看看这个吧。

SearchBar的构造函数中,我们不得不为 Windows 和 Linux 定义一些特殊情况的代码,以便能够绑定我们的EVT_KEY_UP处理程序。这是为了解决一个 bug,由于SearchControl在这两个平台上是一个复合控件,导致KeyEvents无法正确传播。在 Macintosh 上,SearchCtrl是一个原生小部件,因此事件绑定可以正常工作。接下来,在我们的OnEnter事件处理程序中,我们检查控件中的文本值,并根据搜索的上下文生成一个EVT_FINDEVT_FIND_NEXT事件。由于这些是命令事件,调用self.ProcessEvent将在事件处理程序链中启动我们的FIND事件的处理,允许它传播直到被处理。

参见

  • 请参阅第二章中的理解事件传播配方,以了解事件是如何工作的讨论。

  • 请参阅第七章中的使用 BoxSizer配方,窗口布局与设计,以了解如何使用 BoxSizers 进行窗口布局控制。

使用 ListCtrl 混合

类似于 TreeCtrl,有多个混合类(mixin classes)可供扩展标准 ListCtrl 的功能。本食谱介绍了如何使用 CheckListCtrlMixinListRowHighlighterListCtrlAutoWidthMixin 混合类来创建一个 ListCtrl,该控件允许通过使用 CheckBoxes 来选择多个项目。

如何做到这一点...

在这里,我们将定义我们的基础 CheckListCtrl 类,该类使用三个混合类来自定义控件的外观和感觉,并添加复选框:

import wx
import wx.lib.mixins.listctrl as listmix

class CheckListCtrl(wx.ListCtrl,
                    listmix.CheckListCtrlMixin,
                    listmix.ListRowHighlighter,
                    listmix.ListCtrlAutoWidthMixin):
    def __init__(self, *args, **kwargs):
        wx.ListCtrl.__init__(self, *args, **kwargs)
        listmix.CheckListCtrlMixin.__init__(self)
        listmix.ListRowHighlighter.__init__(self)
        listmix.ListCtrlAutoWidthMixin.__init__(self)

        # Attributes
        self._observers = list()

在这里,我们重写了CheckListCtlrMixinOnCheckItem方法,并实现了一个观察者接口,以便在列表中的CheckBox被切换时通知客户端:

    def OnCheckItem(self, index, flag):
        """Overrides CheckListCtrlMixin.OnCheckItem 
        callback"""
        # Notify observers that a checkbox was 
        # checked/unchecked
        for observer in self._observers:
            observer(index, flag)

剩下的就是添加一个GetItems方法来返回已检查项目的列表,以及另一个方法允许客户端注册自己作为当项目在控制中检查时的观察者:

    def GetItems(self, checked=True):
        """Gets a list of all the (un)checked items"""
        indexes = list()
        for index in range(self.GetItemCount()):
            if self.IsChecked(index) == checked:
                indexes.append(index)
        return indexes

    def RegisterObserver(self, callback):
        """Register OnCheckItem callback
        @param callaback: callable(index, checked)
        """
        self._observers.append(callback)

它是如何工作的...

在这个示例中,我们创建了一个通用基类 CheckListCtrl,它将具有以下扩展功能。在列 0 的每一行都将有一个 CheckBox,交替的行将突出显示其背景,而 ListCtrl 的最右侧列将自动调整大小以填充控件中的剩余空间。这些功能分别由 CheckListCtrlMixinListRowHighlighterListCtrlAutoWidthMixin 类提供。

CheckListCtrlMixin 提供了一个可重写的方法,OnCheckItem,当在 ListCtrl 中的某个 CheckBox 被点击时会被调用。我们重写了这个方法,并添加了一种方式让客户端代码可以注册观察者回调方法到该控件中。这样,如果任何使用这个控件的客户端代码希望在 CheckBox 切换时收到通知,它们可以注册自己的观察者方法。

我们CheckListCtrl类的最后一部分是GetItems方法,它是为了使获取控制中所有已勾选或未勾选项的索引列表变得容易而添加的。请参阅随此主题附带的示例代码,以了解使用此新控件的应用示例:

如何工作...

还有更多...

wx.lib.mixins.listctrl 模块为 ListCtrl 提供了几个额外的混合类。以下是这些其他类的快速参考:

混入类 描述
ColumnSorterMixin 帮助处理在点击列标题时控件中项的排序。
TextEditMixin 使之能够编辑多列 ListCtrl 中的任意列文本。

参见

  • 请参阅第四章中的使用 ListCtrl 列出数据配方,用户界面的高级构建块,以了解使用ListCtrl的另一个示例。

  • 请参阅第九章中的实现观察者模式配方,以了解如何使用观察者模式。

样式文本控件自定义高亮

如第四章“用户界面的高级构建块”中“使用 lexers 的 StyledTextCtrl”部分所述,StyledTextCtrl是一个功能强大的源代码编辑组件,它支持多种不同类型源代码的语法高亮。然而,如果你发现你的应用程序需要支持一些StyledTextCtrl没有内置 lexers 的语法高亮,你可能认为你运气不佳。但这并非事实。可以通过使用特殊的容器 lexer 来添加自定义 lexers。本食谱展示了如何编写和使用一个执行一些简单高亮的自定义 lexer。

如何做到这一点...

作为这个菜谱的一部分,我们将创建一个简单的框架,它可以扩展以执行其他类型的突出显示。让我们从定义处理由 StyledTextCtrl: 生成的 EVT_STC_STYLENEEDED 事件的 BaseLexer 类开始。

import wx
import wx.stc

class BaseLexer(object):
    """Defines simple interface for custom lexer objects"""
    def __init__(self):
        super(BaseLexer, self).__init__()

    def StyleText(self, event):
        raise NotImplementedError

接下来,我们有我们的VowelLexer示例实现,它将为文档中所有的元音字母提供文本样式:

class VowelLexer(BaseLexer):
    """Simple lexer to highlight vowels"""
    # Define some style IDs
    STC_STYLE_VOWEL_DEFAULT, \
    STC_STYLE_VOWEL_KW = range(2)
    def __init__(self):
        super(VowelLexer, self).__init__()

        # Attributes
        self.vowels = [ord(char) for char in "aeiouAEIOU"] 

StyleText 方法是我们自定义的 StyledTextCtrl 在其 EVT_STC_STYLENEEDED 事件处理程序中将要委托执行的方法。VowelLexer 支持两种不同的样式:一种用于其默认样式,另一种用于元音字母。

    def StyleText(self, event):
        """Handle the EVT_STC_STYLENEEDED event"""
        stc = event.GetEventObject()
        # Last correctly styled character
        last_styled_pos = stc.GetEndStyled()
        # Get styling range for this call
        line = stc.LineFromPosition(last_styled_pos)
        start_pos = stc.PositionFromLine(line)
        end_pos = event.GetPosition()
        # Walk the line and find all the vowels to style
        # Note: little inefficient doing one char at a time
        #       but just to illustrate the process.
        while start_pos < end_pos:
            stc.StartStyling(start_pos, 0x1f)
            char = stc.GetCharAt(start_pos)
            if char in self.vowels:
                # Set Vowel Keyword style
                style = VowelLexer.STC_STYLE_VOWEL_KW
            else:
                # Set Default style
                style = VowelLexer.STC_STYLE_VOWEL_DEFAULT
            # Set the styling byte information for 1 char from
            # current styling position (start_pos) with the
            # given style.
            stc.SetStyling(1, style)
            start_pos += 1

CustomSTC 类将为使用 BaseLexer-derived 类来定制控件中文本的高亮显示提供框架:

class CustomSTC(wx.stc.StyledTextCtrl):
    def __init__(self, *args, **kwargs):
        super(CustomSTC, self).__init__(*args, **kwargs)

        # Attributes
        self.custlex = None

        # Event Handlers
        self.Bind(wx.stc.EVT_STC_STYLENEEDED, self.OnStyle)

    def OnStyle(self, event):
        # Delegate to custom lexer object if one exists
        if self.custlex:
            self.custlex.StyleText(event)
        else:
            event.Skip()

    def SetLexer(self, lexerid, lexer=None):
        """Overrides StyledTextCtrl.SetLexer
        Adds optional param to pass in custom container
        lexer object.
        """
        self.custlex = lexer
        super(CustomSTC, self).SetLexer(lexerid)

与本章所附的示例代码一起包含的是一个使用上面定义的定制VowelLexer类的简单应用程序。

它是如何工作的...

在这个菜谱中,我们首先为创建针对 StyledTextCtrl 的自定义词法分析器构建了一个小框架。从我们的 BaseLexer 类开始,我们定义了一个简单的接口,用于将处理 EVT_STC_STYLENEEDED 任务委托给对象。接下来,我们创建了 VowelLexer 类,这是一个简单的 BaseLexer 子类,它将在文档文本中突出显示元音字母。在 StyledTextCtrl 中应用样式涉及三个基本步骤。首先,你需要调用 StartStyling 来指示你希望在缓冲区中开始样式的位置,然后你需要确定要设置的样式字节,最后你需要调用 SetStyling 来设置从起始位置开始应用给定样式的字符数。

现在为了让 StyledTextCtrl 能够使用这些词法分析器,我们需要做一些事情,这些事情我们已经封装在 CustomSTC 类中。StyledTextCtrl 需要绑定到 EVT_STC_STYLENEEDED 并设置 STC_LEX_CONTAINER 词法分析器。当容器词法分析器是当前词法分析器,并且检测到缓冲区中的某些文本可能因为内容的变化需要重新格式化时,StyledTextCtrl 将会生成 EVT_STC_STYLEDNEEDED 事件。为了在我们的 CustomSTC 类中处理这种情况,我们只需将事件委托给通过我们重写的 SetLexer 方法设置的当前词法分析器对象。

最后,我们有一个超级简单的示例应用程序,展示了如何在我们的应用程序中使用CustomSTCVowelLexer。首先,我们需要通过调用SetStyleSpec来设置样式,指定为我们的词法分析器的两种样式字节应用哪些颜色。STC_STYLE_VOWEL_DEFAULT将以纯黑色文本样式显示,而STC_STYLE_VOWEL_KW将以红色文本样式显示。然后,剩下的就是调用SetLexer来设置STC_LEX_CONTAINER词法分析器,并为控件创建一个我们的VowelLexer实例。所以运行一下,当你将文本输入到缓冲区时,所有的元音字母都应该被涂成红色。

参见

  • 请参阅第四章中的使用 lexers 的 StyledTextCtrl配方,用户界面的高级构建块,以了解使用StyleTextCtrl的另一个示例。

创建自定义控件

在某个时候,你可能需要发明一个全新的控件来满足你应用程序的一些特定需求。因此,在这个菜谱中,我们将探讨一些从头开始创建新控件的技术。我们将创建一个自定义的CheckBox控件,其标签位于CheckBox下方。

如何做到这一点...

要开始,我们将定义CustomCheckBox控制器的构造函数为PyControl的子类。在构造函数中,我们将绑定一系列事件,这将使我们能够定义控制器的行为:

class CustomCheckBox(wx.PyControl):
    """Custom CheckBox implementation where label is
    below the CheckBox.
    """
    def __init__(self, parent, id_=wx.ID_ANY, label=""):
        style = wx.BORDER_NONE
        super(CustomCheckBox, self).__init__(parent,
                                             id_,
                                             style=style)

        # Attributes
        self.InheritAttributes()
        self._hstate = 0
        self._checked = False
        self._ldown = False
        self.SetLabel(label)

        # Event Handlers
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_ENTER_WINDOW,
                  lambda event:
                  self._SetState(wx.CONTROL_CURRENT))
        self.Bind(wx.EVT_LEAVE_WINDOW,
                  lambda event: self._SetState(0))

接下来,我们有一个辅助方法来帮助管理控制状态与鼠标的关系:

    def _SetState(self, state):
        if self._hstate != state:
            if state == 0:
                self._ldown = False
            self._hstate = state
            self.Refresh()

    #-- Implementation --#

这是对PyControlDoGetBestSize方法的虚拟覆盖,用于控制控件的大小:

    def DoGetBestSize(self):
        lblsz = self.GetTextExtent(self.GetLabel())
        width = max(lblsz[0], 16) + 4 # 2px padding l/r
        height = lblsz[1] + 16 + 6
        best_sz = wx.Size(width, height)
        self.CacheBestSize(best_sz)
        return best_sz

    #-- Event Handlers --#

接下来,我们关注将定义控件行为的处理事件。首先,在OnPaint中,我们进行绘制操作,赋予控件外观:

    def OnPaint(self, event):
        dc = wx.AutoBufferedPaintDCFactory(self)
        gc = wx.GCDC(dc)
        renderer = wx.RendererNative.Get()

        # Setup GCDC
        rect = self.GetClientRect()
        bcolour = self.GetBackgroundColour()
        brush = wx.Brush(bcolour)
        gc.SetBackground(brush)
        gc.Clear()

        # Center checkbox
        cb_x = (rect.width - 16) / 2
        cb_y = 2 # padding from top
        cb_rect = wx.Rect(cb_x, cb_y, 16, 16)

        # Draw the checkbox
        state = 0
        if self._checked:
            state = wx.CONTROL_CHECKED
        if not self.IsEnabled():
            state |= wx.CONTROL_DISABLED
        renderer.DrawCheckBox(self, dc, cb_rect,
                              state|self._hstate)

        # Draw the label
        lbl_rect = wx.Rect(0, cb_rect.bottom, rect.width,
                           rect.height - cb_rect.height)
        gc.DrawLabel(self.GetLabel(),
                     lbl_rect,
                     wx.ALIGN_CENTER)

    def OnErase(self, event):
        pass # do nothing

下两个事件处理器管理控件中的鼠标点击状态,以切换CheckBox的状态:

    def OnLeftDown(self, event):
        self._ldown = True
        event.Skip()

    def OnLeftUp(self, event):
        if self._ldown:
            self._ldown = False
            self._checked = not self._checked
            self.Refresh()
            # Generate EVT_CHECKBOX event
            etype = wx.wxEVT_COMMAND_CHECKBOX_CLICKED
            chevent = wx.CommandEvent(etype, self.GetId())
            chevent.SetEventObject(self)
            self.ProcessEvent(chevent)
        event.Skip()

最后但同样重要的是,我们定义了几种方法来实现wx.CheckBox接口的一部分:

    #---- Public Api ----#

    def SetValue(self, state):
        self._checked = state
        self.Refresh()

    def GetValue(self):
        return self._checked

    def IsChecked(self):
        return self.GetValue()

它是如何工作的...

这是一种相对简单的控制实现,但它是一个很好的例子,展示了在创建自己的自定义控制时可以采取的一些方法。因此,让我们逐一分析每个重要部分,看看它们是如何影响控制工作方式的。

首先,在构造函数中,我们定义了三个属性来管理控制器的状态:

  1. self._hstate: 用于存储控制器的当前高亮状态。

  2. self._checked: 用于保存 CheckBox 的状态。

  3. self._ldown: 当在控件中点击了左鼠标按钮时保持。

接下来,我们需要绑定到绘制控件和实现其行为所必需的事件。我们使用了两个绘图事件和四个不同的鼠标事件。首先,让我们看看用于实现控件行为的鼠标事件处理器。

OnLeftDown 中,我们只需将我们的 self._ldown 标志设置为 True,以指示下点击动作是在此窗口中而不是在其他地方发起的。然后,在 OnLeftUp 处理程序中,如果 self._ldown 标志为 True,我们将 self._checked 标志切换以反映新的 CheckBox 状态,然后调用 Refresh。调用 Refresh 将会生成一个 EVT_PAINT 事件,这样我们就可以使用我们的 OnPaint 处理程序重新绘制控件以反映其新状态。之后,我们还会生成一个 EVT_CHECKBOX 事件,以便通知应用程序 CheckBox 状态已更改。剩余的两个鼠标事件用于在鼠标进入或离开控件区域时更新控件的高亮状态。

OnPaint 是我们绘制控件并赋予其外观的地方。我们在 OnPaint 中创建绘图上下文并设置背景。接下来,我们计算在控件矩形内绘制 CheckBox 的位置,并使用 RendererNative 根据控件的当前状态绘制 CheckBox。然后,剩下的就是使用我们的 GCDC's DrawLabel 方法在 CheckBox 下方绘制标签。

为了完成控制功能,我们添加了一些方法来实现CheckBox部分常规界面的功能,以便使用此控件的应用程序代码可以获取和设置CheckBox的状态:

如何工作...

参见

  • 请参阅第一章中的理解继承限制配方,以了解关于重写虚拟方法的讨论。

  • 查阅第二章中的“理解事件传播”配方,响应事件以获取更多关于处理事件的详细信息。

  • 请参阅第八章中的使用 RendererNative 进行绘图配方,屏幕绘图部分提供了使用RendererNative绘制原生外观控件的其他示例。

  • 请参阅第八章中的减少绘图过程中的闪烁配方,屏幕绘图章节,以了解如何减少绘图过程中的闪烁。

第十一章:使用线程和计时器创建响应式界面

在本章中,我们将涵盖:

  • 非阻塞 GUI

  • 理解线程安全性

  • 线程工具

  • 使用计时器

  • 捕获输出

简介

当你使用一个应用程序并点击某个按钮或控件时,突然发现应用程序的 UI 似乎停止响应,忙碌的光标出现,而你只能在那里猜测应用程序是否还在工作,或者它是否已经锁定并需要强制退出。这种不愉快的体验几乎总是由于一个在调用后需要相当长时间才能返回结果的函数或动作造成的。如果这个函数或动作是在 GUI 对象所在的同一线程上被调用的,它将阻塞所有在后台运行并管理 GUI 的代码,导致这个锁定且无响应的界面。

能够以防止这种情况出现在用户面前的方式设计应用程序,与大多数传统的程序性方法相比,需要额外的考虑。本章通过提供解决方案,并希望提供所有必要的工具,来探讨这个问题,以构建高度响应的、多线程的 wxPython 应用程序。

非阻塞图形用户界面

在这个菜谱中,我们探讨什么是响应式界面,并试图对其他菜谱章节中将要提供的解决方案所针对的问题有一个良好的理解。该菜谱创建了一个包含两个按钮的简单应用程序。每个按钮将执行完全相同的任务。然而,由于控制流的执行方式不同,两个按钮在点击按钮后对用户响应和提供反馈的方式将大相径庭。

如何做到这一点...

为了说明当前的问题,我们将创建一个简单的斐波那契数计算器应用程序。首先,我们将从定义一个线程类和用于计算第 N 个斐波那契数的函数开始:

import wx
import threading

class FibThread(threading.Thread):
    def __init__(self, window, n):
        super(FibThread, self).__init__()

        # Attributes
        self.window = window
        self.n = n

    def run(self):
        val = SlowFib(self.n)
        wx.CallAfter(self.window.output.SetValue, str(val))
        wx.CallAfter(self.window.StopBusy)

def SlowFib(n):
    """Calculate Fibonacci numbers
    using slow recursive method to demonstrate
    blocking the UI.
    """
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return SlowFib(n-1) + SlowFib(n-2)

现在让我们为我们的斐波那契数计算器创建用户界面:

class BlockingApp(wx.App):
    def OnInit(self):
        self.frame = BlockingFrame(None,
                                   title="Non-Blocking Gui")
        self.frame.Show()
        return True

class BlockingFrame(wx.Frame):
    """Main application window"""
    def __init__(self, *args, **kwargs):
        super(BlockingFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = BlockingPanel(self)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize()

在这里,在面板中,大部分的操作将在本例中发生。在这里,我们布局了一个简单的界面,包括一个输入字段和一个输出字段,两个按钮,以及一个进度条:

class BlockingPanel(wx.Panel):
    def __init__(self, parent):
        super(BlockingPanel, self).__init__(parent)

        # Attributes
        self.timer = wx.Timer(self)
        self.input = wx.SpinCtrl(self, value="35", min=1)
        self.output = wx.TextCtrl(self)
        self.block = wx.Button(self, label="Blocking")
        self.noblock = wx.Button(self, label="Non-Blocking")
        self.prog = wx.Gauge(self)

        # Layout
        self.__DoLayout()

        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnButton)
        self.Bind(wx.EVT_TIMER, self.OnPulse, self.timer)

    def __DoLayout(self):
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        gridsz = wx.GridSizer(2, 2, 5, 5)

        # Layout controls
        vsizer.Add(self.prog, 0, wx.EXPAND)
        gridsz.Add(wx.StaticText(self, label="fib(n):"))
        gridsz.Add(self.input, 0, wx.EXPAND)
        gridsz.Add(wx.StaticText(self, label="result:"))
        gridsz.Add(self.output, 0, wx.EXPAND)
        vsizer.Add(gridsz, 0, wx.EXPAND|wx.ALL, 10)
        hsizer.Add(self.block, 0, wx.ALL, 5)
        hsizer.Add(self.noblock, 0, wx.ALL, 5)
        vsizer.Add(hsizer, 0, wx.ALIGN_CENTER_HORIZONTAL)

        self.SetSizer(vsizer)

在我们的EVT_BUTTON处理程序中,我们进行计算。首先,我们清除当前输出,然后启动进度Gauge。之后,我们根据点击的是哪个Button选择两条路径之一。如果是“Blocking”按钮,我们就在同一个线程中直接进行计算。如果是“Non-Blocking”按钮被点击,我们将任务委托给后台线程,以便允许 GUI 继续处理:

    def OnButton(self, event):
        input = self.input.GetValue()
        self.output.SetValue("") # clear output
        self.StartBusy() # give busy feedback
        if event.GetEventObject() == self.block:
            # Calculate value in blocking mode
            val = SlowFib(input)
            self.output.SetValue(str(val))
            self.StopBusy()
        else:
            # Non-Blocking mode
            task = FibThread(self, input)
            task.start()

这些方法被添加以控制进度量表并更新 GUI 的状态,具体取决于应用程序是否正在计算:

    def OnPulse(self, event):
        self.prog.Pulse() # Pulse busy feedback

    def StartBusy(self):
        self.timer.Start(100)
        self.block.Disable()
        self.noblock.Disable()

    def StopBusy(self):
        self.timer.Stop()
        self.prog.SetValue(0)
        self.block.Enable()
        self.noblock.Enable()

if __name__ == '__main__':
    app = BlockingApp(False)
    app.MainLoop()

它是如何工作的...

运行上一段代码将会显示以下应用程序窗口:

如何工作...

此应用程序将计算由第一个字段指定的第 N 个斐波那契数。使用 35 或更高的数字,通过使用SlowFib函数进行计算将需要从几秒到几分钟不等。点击两个按钮中的任意一个,都会调用相同的SlowFib函数,并最终生成相同的结果。因此,带着这个想法,让我们跳转到BlockingPanelOnButton方法,看看两个按钮之间有什么不同之处。

当调用OnButton时,我们首先清除结果字段,然后Start启动TimerPulse窗口顶部的Gauge,从而给用户反馈我们正在忙于计算结果。如果点击了阻塞按钮,我们直接调用SlowFib函数来获取结果。在此阶段,应用程序的控制流将停滞等待SlowFib返回,这意味着MainLoop将等待我们的OnButton方法返回。由于OnButton只有在SlowFib完成后才会返回,因此框架将无法处理任何事件,例如重绘窗口、鼠标点击或我们的TimerEvent以脉冲Gauge。正因为如此,阻塞按钮仍然看起来是按下的,并且Frame及其所有控件将完全无响应,直到SlowFib完成并返回控制权到MainLoop

如何工作...

相反,如果你点击了非阻塞按钮,我们仍然运行相同的SlowFib函数,但是在一个单独的Thread中执行。这允许OnButton立即返回,将控制权交还给MainLoop。因此,由于MainLoop没有被OnButton阻塞,它可以自由地处理其他事件,允许我们的忙碌指示器更新,按钮显示为禁用状态,以及Frame可以在桌面上自由移动。当FibThread上的计算完成时,它使用CallAfter函数发送消息来调用所需的功能,以在主线程上更新 GUI,然后退出,使 GUI 准备好开始另一个计算:

如何工作...

两个按钮产生结果所需的时间大致相同,但非阻塞按钮允许 GUI 继续平稳运行,并会给用户留下软件正在忙碌工作的良好印象,知道软件并未锁定。

参见

  • 有关创建线程安全 GUI 的更多信息,请参阅本章中的理解线程安全配方。

  • 请参阅本章中的使用计时器配方,了解在执行长时间运行的任务时保持 GUI 响应的其他方法。

理解线程安全性

几乎所有用户界面工具包都是设计在单个执行线程中运行的。了解如何在多线程应用程序中与其他工作线程交互 GUI 线程是一项重要的任务,需要谨慎执行,以避免应用程序中出现看似无法解释且随机的崩溃。这在 wxPython 中与其他典型的 GUI 工具包一样适用。

在 wxPython 应用程序中保持线程安全可以通过几种不同的方式来处理,但使用事件是最典型的方法。由MainLoop监控的事件队列提供了一种线程安全的方式来从后台线程传递数据和动作,以便在 GUI 线程的上下文中进行处理。本食谱展示了如何使用自定义事件和PostEvent函数来更新存在于主 GUI 线程中的 GUI 对象。

如何做到这一点...

由于我们将在示例应用程序中使用事件来维护线程安全,我们首先将定义一个自定义事件类型:

import wx
import time
import threading

# Define a new custom event type
wxEVT_THREAD_UPDATE = wx.NewEventType()
EVT_THREAD_UPDATE = wx.PyEventBinder(wxEVT_THREAD_UPDATE, 1)

class ThreadUpdateEvent(wx.PyCommandEvent):
    def __init__(self, eventType, id):
        super(ThreadUpdateEvent, self).__init__(eventType, id)

        # Attributes
        self.value = None

    def SetValue(self, value):
        self.value = value

    def GetValue(self):
        return self.value

下面的CountingThread类将被用作本应用程序的后台工作线程,并使用之前的事件类来通知并在主 GUI 线程上执行更新:

class CountingThread(threading.Thread):
    """Simple thread that sends an update to its 
    target window once a second with the new count value.
    """
    def __init__(self, targetwin, id):
        super(CountingThread, self).__init__()

        # Attributes
        self.window = targetwin
        self.id = id
        self.count = 0
        self.stop = False

    def run(self):
        while not self.stop:
            time.sleep(1) # wait a second
            # Notify the main thread itsit’s time 
            # to update the ui
            if self.window:
                event = ThreadUpdateEvent(wxEVT_THREAD_UPDATE,
                                          self.id)
                event.SetValue(self.count)
                wx.PostEvent(self.window, event)
            self.count += 1

    def Stop(self):
        # Stop the thread
        self.stop = True

class ThreadSafetyApp(wx.App):
    def OnInit(self):
        self.frame = ThreadSafeFrame(None,
                                     title="Thread Safety")
        self.frame.Show()
        return True

从这里开始,我们将使用ThreadSafeFrame类来创建应用程序的 GUI。框架将是来自CountingThread:更新的目标:

class ThreadSafeFrame(wx.Frame):
    """Main application window"""
    def __init__(self, *args, **kwargs):
        super(ThreadSafeFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = ThreadSafePanel(self)
        self.threadId = wx.NewId()
        self.worker = CountingThread(self, self.threadId)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize((300, 300))

        # Start the worker thread
        self.worker.start()

        # Event Handlers
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(EVT_THREAD_UPDATE, self.OnThreadEvent)

    def OnClose(self, event):
        # Stop the worker thread
        self.worker.Stop()
        event.Skip()

这里是绑定到自定义ThreadUpdateEvent类的EVT_THREAD_UPDATE事件绑定器的ThreadSafeFrame的事件处理器。此方法将在CountingThread发布新的更新事件后,在主 GUI 线程上执行 GUI 更新时被调用:

    def OnThreadEvent(self, event):
        if event.GetId() == self.threadId():
            # Handle event to update Displayed Count
            value = event.GetValue()
            self.panel.DisplayCount(value)
        else:
            event.Skip()

class ThreadSafePanel(wx.Panel):
    def __init__(self, parent):
        super(ThreadSafePanel, self).__init__(parent)

        # Attributes
        self.count = wx.StaticText(self, label="Count: ")

        # Setup
        self.__DoLayout()

    def __DoLayout(self):
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        vsizer.AddStretchSpacer()
        hsizer.AddStretchSpacer()
        hsizer.Add(self.count)
        hsizer.AddStretchSpacer()
        vsizer.Add(hsizer, 0, wx.EXPAND)
        vsizer.AddStretchSpacer()
        self.SetSizer(vsizer)

    def DisplayCount(self, value):
        self.count.SetLabel("Count: %d" % value)

if __name__ == '__main__':
    app = ThreadSafetyApp(False)
    app.MainLoop()

它是如何工作的...

这个菜谱的目的是展示一个从后台线程更新 GUI 的通用模式。为了说明这一点,我们创建了一个简单的Frame,它包含一个带有单个StaticTextCtrlPanel,该StaticTextCtrl将在CountingThread完成其艰巨的任务(每次将计数增加一)后进行更新。

首先,我们创建了一个新的事件类型ThreadUpdateEvent,以及相关的事件绑定器,用于在需要告诉 UI 更新显示值时,从CountingThread传输数据到主线程。ThreadUpdateEventCountingThreadrun方法中使用,通过传递给PostEvent来使用,这是一种线程安全的方式来为主 GUI 线程排队一些工作。

PostEvent会将事件对象放入MainLoop的事件队列中,这样在它完成处理任何当前任务后,它将抓取并派发这个更新事件到我们ThreadSafeFrame中适当的事件处理器。这是使得从后台线程安全更新 GUI 成为可能的关键。

如果我们直接在CountingThread的上下文中调用Panel's DisplayCount方法,无法保证两个线程不会同时尝试访问或修改相同的数据。例如,如果 GUI 线程正在处理一个内部的PaintEvent以重新绘制StaticTextCtrl,控制器的标签值将被访问。如果在同一时间,在CountingThread中,它试图更改该值,则可能会发生潜在的内存损坏,这会导致应用程序崩溃。通过使用事件,更新将在主线程完成任何其他挂起的任务后,在主线程的上下文中进行处理,从而消除碰撞的风险,因为对变量的访问将由主线程以序列化的方式进行控制。

还有更多...

wxPython 提供的CallAfter函数也可以用于从后台线程调用影响 GUI 对象的方法。CallAfter函数内部封装了大部分事件创建和处理过程,因此在进行简单的 GUI 更改时,它可能更加方便和透明,正如我们在本食谱中所做的那样。因此,让我们简要了解一下CallAfter是如何工作以及如何使用的:

wx.CallAfter(callable, *args, **kw)

CallAfter 函数将其第一个参数接受为一个函数。*args**kw 用于指定任何应传递给第一个参数指定的函数的定位或关键字参数。因此,例如,我们可以用以下代码替换掉我们 CountingThreadrun 方法中创建和发送自定义事件的三个代码行:

wx.CallAfter(self.window.panel.DisplayCount,
             self.count)

现在,CallAfter 函数将在主 GUI 线程上创建并发布一个事件,该事件包含函数及其参数到 App 对象。当 MainLoop 到达处理此事件时,它将由属于 App 对象的事件处理器处理。然后,此事件处理器将简单地使用其指定的任何参数调用该函数,因此它将在主 GUI 线程的上下文中被调用。

重要的是要理解CallAfter的确切含义——该方法将在MainLoop的下一个迭代之后被调用。因此,你不能期望从传递给它的方法中获取返回值,因为它将在你进行CallAfter调用作用域之外异步调用。所以,为了明确起见,CallAfter总是返回None,这意味着你不能用它来编写如下代码:

value = wx.CallAfter(window.GetValue)

这是因为 window.GetValue 实际上是在 CallAfter 函数返回之后才被调用的。

参见

  • 请参阅第二章中的创建自定义事件类配方,响应事件部分提供了创建自定义事件的另一个示例。

  • 请参阅本章中的 线程工具 菜单以获取更多示例和保持与后台线程一起工作时 GUI 线程安全性的方法。

线程工具

维护线程安全性有时可能会很繁琐和困难,因此在本教程中,我们将创建三个有用的工具,使线程操作变得更加容易。我们将创建两个装饰器函数和一个元类,以帮助将线程安全规则应用到方法和函数中,使其变得像添加一行代码一样简单。

如何做到这一点...

在这里,将创建一个小的实用模块,它可以被用来帮助任何需要与线程一起工作的 wxPython 应用程序:

import wx
import threading
from types import FunctionType, MethodType

__all__ = ['callafter', 'synchfunct', 'ClassSynchronizer']

从这里开始,我们将创建一个简单的装饰器函数,它可以用来装饰 GUI 类中的任何方法,这样如果从后台线程调用被装饰的方法,它将自动将调用委托给CallAfter函数:

def callafter(funct):callafter(funct):
    """Decorator to automatically use CallAfter if
    a method is called from a different thread.
    """
    def callafterwrap(*args, **kwargs):
        if wx.Thread_IsMain():
            return funct(*args, **kwargs)
        else:
            wx.CallAfter(funct, *args, **kwargs)
    callafterwrap.__name__ = funct.__name__
    callafterwrap.__doc__ = funct.__doc__
    return callafterwrap

接下来是Synchronizer类,它被用作辅助类来同步对主 GUI 线程的异步调用:

class Synchronizer(object):
    """Synchronize CallAfter calls"""
    def __init__(self, funct, args, kwargs):
        super(Synchronizer, self).__init__()

        # Attributes
        self.funct = funct
        self.args = args
        self.kwargs = kwargs
        self._synch = threading.Semaphore(0)

此方法将由该类的Run方法通过CallAfter在主 GUI 线程上执行调用。它只是调用该函数并释放Semaphore:

    def _AsynchWrapper(self):
        """This part runs in main gui thread"""
        try:
            self.result = self.funct(*self.args,
                                     **self.kwargs)
        except Exception, msg:
            # Store exception to report back to
            # the calling thread.
            self.exception = msg
        # Release Semaphore to allow processing back 
        # on other thread to resume.
        self._synch.release()

Run 方法由后台线程调用,并使用 CallAfter 将函数调用委托给主 GUI 线程。然后它获取Semaphore,这样执行就会在后台线程中的该行暂停,直到 _AsyncWrapper 方法调用 release:

    def Run(self):
        """Call from background thread"""
        # Make sure this is not called from main thread
        # as it will result in deadlock waiting on the
        # Semaphore.
        assert not wx.Thread_IsMain(), "Deadlock!"
        # Make the asynchronous call to the main thread
        # to run the function.
        wx.CallAfter(self._AsynchWrapper)
        # Block on Semaphore release until the function
        # has been processed in the main thread by the
        # UI's event loop.
        self._synch.acquire()
        # Return result to caller or raise error
        try:
            return self.result
        except AttributeError:
            raise self.exception

接下来是 syncfunct 装饰器,它的工作方式与 CallAfter 装饰器相同,只不过它使用 Synchronizer 来使后台线程的调用变为同步:

def synchfunct(funct):
    """Decorator to synchronize a method call from a worker
    thread to the GUI thread.
    """
    def synchwrap(*args, **kwargs):
        if wx.Thread_IsMain():
            # called in context of main thread so
            # no need for special synchronization
            return self.funct(*args, **kwargs)
        else:
            synchobj = Synchronizer(funct, args, kwargs)
            return synchobj.Run()

    synchwrap.__name__ = funct.__name__
    synchwrap.__doc__ = funct.__doc__
    return synchwrap

本模块将展示的最终实用工具是 ClassSynchronizer 元类,它可以用来自动将 synchfunct 装饰器应用到类中的所有方法:

class ClassSynchronizer(type):
    """Metaclass to make all methods in a class threadsafe"""
    def __call__(mcs, *args, **kwargs):
        obj = type.__call__(mcs, *args, **kwargs)

        # Wrap all methods/functions in the class with
        # the synchfunct decorator.
        for attrname in dir(obj):
            attr = getattr(obj, attrname)
            if type(attr) in (MethodType, FunctionType):
                nfunct = synchfunct(attr)
                setattr(obj, attrname, nfunct)

        return obj

它是如何工作的...

如果你之前没有使用过装饰器和元类,它们可能会让你一开始感到有些 intimidating,所以让我们逐一查看我们这三个新的实用工具,看看它们是如何工作的,以及如何在你的代码中使用它们。

第一个实用工具是 callafter 装饰器。这是一个非常简单的装饰器,当从非 GUI 线程的线程调用时,它将仅将函数包装在 CallAfter 中。由于它使用 CallAfter,此装饰器仅应用于不需要返回值的函数,例如设置值或执行不需要在后台线程中获取反馈的更新。此装饰器的使用非常简单。请参阅以下示例片段:

class MyPanel(wx.Panel):
    @callafter
    def SetSomeGuiValues(self, values):
        self.ctrl1.SetValue(values[0])
        ...
        self.ctrlN.SetValue(values[N])

现在,SetSomeGuiValues 方法可以从应用程序中的任何线程调用。装饰器函数接受另一个函数作为参数,并返回一个新的函数,该函数通常将现有函数包装在某种新行为中。因此,当我们的模块由 Python 初始化时,它将看到类中的装饰器参数将装饰器应用于函数,然后将函数重新绑定到装饰器返回的新函数。我们的 callafter 装饰器简单地将给定的函数包装在一个检查中,以查看它是否是从主线程调用的,如果不是,它将使用 CallAfter 来运行该函数。

接下来是 synchfunct 装饰器。这个装饰器使用我们的 Synchronizer 类来实现对函数间线程调用的同步。当后台线程需要以同步方式调用 GUI 来获取值时,可以使用此方法。synchfunct 装饰器的工作方式几乎与我们的 callafter 装饰器相同,因此让我们看看 Synchronizer 是如何使异步的 CallAfter 函数变为同步调用的。

Synchronizer 类,就像 CallAfter 函数一样,接受一个函数及其任何参数作为参数来初始化它。它还从 threading 模块创建一个 Semaphore 对象,用于同步操作。SynchronizerRun 方法使用 CallAfter 来调用传入的函数。在调用 CallAfter 之后,Run 方法将在 Semaphoreacquire 调用上阻塞。这将停止 Run 函数中其余代码的执行以及后台线程,直到 _AsynchWrapper 方法在主线程上完成对传入函数的运行后,在 Semaphore 上调用 release。当调用 release 后,Run 方法将继续执行其 acquire 调用之后的部分,并将返回在主线程上运行的函数的结果,或者如果调用该方法时引发了异常,则抛出异常。

最后,我们有ClassSynchronizer元类。这个元类将使用synchfunct装饰器来使类中的每个方法都成为线程安全的。首先,让我们快速看一下下面的代码片段,以便展示如何使用这个元类,然后我们将检查它是如何工作的:

class MyPanel(wx.Panel):
    __metaclass__ = ClassSynchronizer
    def __init__(self, parent, *args, **kwargs)

没有什么比这更简单了,对吧?当 Python 解释器初始化类时,它会看到我们的 __metaclass__ 声明,这将导致我们的 ClassSynchronizer__call__ 方法被调用。在 __call__ 中,我们使用 dir 来枚举给定类字典中的所有项。然后,对于类中的每个是 MethodTypeFunctionType 的项,我们应用 synchfunct 装饰器以获取方法的新包装版本,然后使用包装后的版本来替换它。

还有更多...

伴随本主题的示例代码中包含了上面显示的完整 threadtools 模块,以及一个示例应用程序,该应用程序展示了 callaftersyncfunct 装饰器的一些额外使用示例,以及在一个从给定 URL 获取 HTML 并在 TextCtrl 中显示的应用程序中的 ClassSynchronizer 元类。

参见

  • 有关使用线程与 GUI 的更多信息,请参阅本章中的理解线程安全性配方。

  • 请参阅第九章中的使用装饰器配方,以了解使用装饰器函数的另一个示例,所在章节为设计方法和技巧

使用计时器

计时器是一个可以创建的对象,用于定期发送事件。通常,计时器用于运行短小的原子任务,例如状态检查和更新,但也可以通过分步执行任务而不是一个长时间的阻塞调用,来利用它保持 UI 在长时间运行的任务中活跃。然而,由于计时器将在主 GUI 线程的上下文中运行,因此有必要设计长时间运行任务的执行,使其能够以几个较小的增量步骤进行,否则在处理TimerEvent时 UI 仍然会锁定。这个配方通过使用计时器创建了一个简单的框架来处理长时间运行的任务。

如何做到这一点...

首先,我们将创建一个基类,该类定义了一个任务可以从中派生的接口:

class TimerTaskBase(object):
    """Defines interface for long running task
    state machine.
    """
    TASK_STATE_PENDING, \
    TASK_STATE_RUNNING, \
    TASK_STATE_COMPLETE = range(3)
    def __init__(self):
        super(TimerTaskBase, self).__init__()

        # Attributes
        self._state = TimerTaskBase.TASK_STATE_PENDING

    #---- Interface ----#

    def ProcessNext(self):
        """Do next iteration of task
        @note: must be implemented by subclass
        """
        raise NotImplementedError

    def InitTask(self):
        """Optional override called before task 
        processing begins
        """
        self.SetState(TimerTaskBase.TASK_STATE_RUNNING)

    #---- Implementation ----#

    def GetState(self):
        return self._state

    def SetState(self, state):
        self._state = state

接下来,可以使用TimerTaskMixin类将使用Timer处理TimerTaskBase 派生的任务对象的功能添加到任何窗口类中:

class TimerTaskMixin(object):
    """Mixin class for a wxWindow object to use timers
    for running long task. Must be used as a mixin with
    a wxWindow derived class!
    """
    def __init__(self):
        super(TimerTaskMixin, self).__init__()

        # Attributes
        self._task = None
        self._timer = wx.Timer(self)

        # Event Handlers
        self.Bind(wx.EVT_TIMER, self.OnTimer, self._timer)

    def __del__(self):
        # Make sure timer is stopped
        self.StopProcessing()

OnTimer 方法将在每 100 毫秒被调用一次,当 Timer 生成一个新事件时。每次调用时,它将调用 TimerTask 对象的 ProcessNext 方法,以便它能够执行其处理过程中的下一步:

    def OnTimer(self, event):
        if self._task is not None:
            self._task.ProcessNext()
            state = self._task.GetState()
            if state == self._task.TASK_STATE_COMPLETE:
                self._timer.Stop()

    def StartTask(self, taskobj):
        assert not self._timer.IsRunning(), \
               "Task already busy!"
        assert isinstance(taskobj, TimerTaskBase)
        self._task = taskobj
        self._task.InitTask()
        self._timer.Start(100)

    def StopProcessing(self):
        if self._timer.IsRunning():
            self._timer.Stop()

它是如何工作的...

首先,让我们看一下我们的 TimerTaskBase 类,它定义了我们的 TimerTaskMixin 类将用于执行长时间运行任务的基本接口。TimerTaskBase 类非常简单。它提供了一个 ProcessNext 方法,该方法必须由子类重写以实现任务工作下一块的处理。每次 TimerTaskMixin 类的计时器触发 TimerEvent 时,都会调用此方法。另一个方法 InitTask 是子类可选的重写,用于实现。它将在第一次调用 ProcessNext 之前立即被调用,并可用于执行任务在处理之前可能需要的任何设置。

TimerTaskMixin 类是一个混合类,它可以与任何 wx.Window-derived 类一起使用,例如 FramePanel。它为管理 TimerTask 对象提供了框架。这个简单的框架添加了一个 StartTask 方法,该方法可以被 UI 用于启动 TimerTask 的处理。StartTask 接收需要处理的 TimerTask 对象,然后启动 TimerTimer 将每 100 毫秒触发一次,以调用任务的 ProcessNext 方法,直到任务报告其状态处于完成状态。

还有更多...

在本主题所附带的完整示例代码中,有一个简单的示例应用程序,它使用此框架将一串 DNA 代码转录为 RNA,作为将更大任务分解为许多较小任务并在计时器事件中处理的示例。

参见

  • 请参阅第二章中的处理事件配方,响应事件章节以获取更多关于事件处理的信息。

  • 有关使用混合类(mixin classes)的更多信息及示例,请参阅第九章中的使用混合类配方,设计方法和技巧

捕获输出

这个配方使用了本章前面提出的一些概念,来创建一个OutputWindow组件,它可以用来捕获子进程的控制台输出并将其重定向到应用程序中的文本显示。它将使用ThreadsTimers来实现对这个任务的高性能解决方案,所以让我们开始并查看代码。

注意事项

当在 Windows 上运行时,此配方使用 pywin32 扩展模块。(sourceforge.net/projects/pywin32/)

如何做到这一点...

在这个菜谱中,我们将创建两个类。第一个将是一个工作线程类,它将运行subprocess并将输出报告给 GUI。第二个将是一个 GUI 组件,它将使用工作线程并显示其输出:

import wx
import wx.stc as stc
import threading
import subprocess

ProcessThread 类将运行一个 subprocess 并从其输出管道读取进程的输出,然后将数据传递回线程的 parent 对象:

class ProcessThread(threading.Thread):
    """Helper Class for OutputWindow to run a subprocess in
    a separate thread and report its output back 
    to the window.
    """
    def __init__(self, parent, command, readblock=4096):
        """
        @param parent: OutputWindow instance
        @param command: Command line command
        @param readblock: amount to read from stdout each read
        """
        assert hasattr(parent, 'AppendUpdate')
        assert readblock > 0
        super(ProcessThread, self).__init__()

        # Attributes
        self._proc = None
        self._parent = parent
        self._command = command
        self._readblock = readblock

        # Setup
        self.setDaemon(True)

在这里的ProcessThreadrun方法中,我们使用 Python 标准库中的subprocess模块来启动和运行我们想要获取输出的进程:

    def run(self):
        # Suppress popping up a console window
        # when running on windows
        if subprocess.mswindows:            
            suinfo = subprocess.STARTUPINFO()
            try:
                from win32process import STARTF_USESHOWWINDOW
                suinfo.dwFlags |= STARTF_USESHOWWINDOW
            except ImportError:
                # Give up and use hard coded value 
                # from Windows.h
                suinfo.dwFlags |= 0x00000001
        else:
            suinfo = None

        try:
            # Start the subprocess
            outmode = subprocess.PIPE
            errmode = subprocess.STDOUT
            self._proc = subprocess.Popen(self._command,
                                          stdout=outmode,
                                          stderr=errmode,
                                          shell=True,
                                          startupinfo=suinfo)
        except OSError, msg:
            self._parent.AppendUpdate(unicode(msg))
            return

在这里,我们只需循环直到进程正在运行,读取其输出并将其附加到parent对象的更新队列中:

        # Read from stdout while there is output from process
        while True:
            self._proc.poll()
            if self._proc.returncode is None:
                # Still running so check stdout
                txt = self._proc.stdout.read(self._readblock)
                if txt:
                    # Add to UI's update queue
                    self._parent.AppendUpdate(txt)
            else:
                break

接下来,我们有用于显示进程输出的 GUI 控件。这个类将使用ProcessThread来运行进程并作为其数据的接收者。它将维护一个线程列表,以便可以同时运行任意数量的进程。

class OutputWindow(stc.StyledTextCtrl):
    def __init__(self, parent):
        super(OutputWindow, self).__init__(parent)

        # Attributes
        self._mutex = threading.Lock()
        self._updating = threading.Condition(self._mutex)
        self._updates = list()
        self._timer = wx.Timer(self)
        self._threads = list()

        # Setup
        self.SetReadOnly(True)

        # Event Handlers
        self.Bind(wx.EVT_TIMER, self.OnTimer)

    def __del__(self):
        if self._timer.IsRunning():
            self._timer.Stop()

AppendUpdate 方法由 ProcessThread 使用,以向此控件传递数据。更新被追加到一个列表中,我们使用锁来保护它,以确保一次只有一个线程可以访问它:

    def AppendUpdate(self, value):
        """Thread safe method to add updates to UI"""
        self._updating.acquire()
        self._updates.append(value)
        self._updating.release()

接下来,我们有一个Timer事件处理器,用于定期检查更新列表并将它们应用到 GUI:

def OnTimer(self, event):
"""Check updates queue and apply to UI"""
# Check Thread(s)
tlist = list(self._threads)
for idx, thread in enumerate(tlist):
if not thread.isAlive():
del self._threads[idx]
# Apply pending updates to control
ind = len(self._updates)
if ind:
# Flush update buffer
self._updating.acquire()
self.SetReadOnly(False)
txt = ''.join(self._updates[:])
self.AppendText(txt)
self.GotoPos(self.GetLength())
self._updates = list()
self.SetReadOnly(True)
self._updating.release()
if not len(self._threads):
self._timer.Stop()

最后,StartProcess 方法是应用程序用来告诉控制器启动一个新进程的方法:

    def StartProcess(self, command, blocksize=4096):
        """Start command. Blocksize can be used to control
        how much text must be read from stdout before window
        will update.
        """
        procthread = ProcessThread(self, command, blocksize)
        procthread.start()
        self._threads.append(procthread)
        if not self._timer.IsRunning():
            self._timer.Start(250)

它是如何工作的...

首先,让我们先看看我们的ProcessThread类。这是OutputWindow用来启动外部进程并捕获其输出的工作线程。构造函数接受三个参数:一个父窗口、一个命令行字符串,以及一个可选的关键字参数,该参数可以指定在每次迭代中从进程的标准输出读取时阻塞的文本量。将readblock参数设置为一个较小的数值将导致ProcessThread有更多的响应式更新。然而,在一个输出大量数据的进程中设置得太低可能会导致许多小而低效的更新。因此,通常最好尝试选择一个尽可能大的值,这个值对于给定进程的输出是合适的。

ProcessThreadrun 方法是它执行所有工作的地方。首先,我们必须处理 Windows 的一个特殊情况,因为 subprocess.POpen 在运行 shell 命令时可能会打开一个命令窗口。我们可以使用 startupflags 来抑制这种行为,因为我们希望将输出显示在我们的 OutputWindow 中。接下来,我们使用 subprocess 模块的 POpen 类来运行构造函数中指定的命令。最后,线程进入一个简单的循环,检查进程是否仍在运行,如果是,则阻塞从进程输出管道中读取指定数量的文本。读取文本后,它调用 OutputWindowAppendUpdate 方法将输出添加到其更新队列中。

现在,让我们看看OutputWindow是如何工作的,用于显示ProcessThread捕获的文本。OutputWindow是从StyledTextCtrl派生出来的,因为这样可以处理比标准TextCtrl更大的文本量,并且通常具有更好的性能,同时提供了一个更强大的 API 来处理缓冲区中的文本,以便我们决定在以后添加一些额外的功能。在OutputWindow的构造函数中,我们做了几件重要的事情。首先,我们创建了一个锁,用于保护更新队列,以确保一次只有一个线程可以修改它。如果第二个线程在另一个线程持有锁的情况下尝试acquire这个锁,它将导致第二个线程在acquire调用处等待,直到其他线程释放锁。第二是更新队列,第三是用于定期轮询更新队列的Timer,最后我们有一个列表来保存已启动的ProcessThread(s)的引用。

OutputWindow 类中剩余的方法都是用来管理它所拥有的 ProcessThread(s) 的更新。StartProcess 方法用于创建并启动一个新的 ProcessThread,如果之前尚未启动,还会启动 OutputWindow 的更新计时器。AppendUpdate 是一个线程安全的方法,供后台线程调用并添加更新到 OutputWindow。这个方法可以直接从后台线程调用,因为它正在修改的数据对象被一个锁保护,这个锁可以防止多个线程同时修改对象。这个方法被选择而不是从工作线程发布事件,因为它可以帮助在大量更新时保持 UI 的响应性,因为它允许将 UI 更新分组为更少的较大更新,而不是许多小更新,这可能导致在处理所有事件时 UI 被锁定。最后但同样重要的是 OnTimer 方法,在这里实际发生 UI 更新。OnTimer 首先检查并移除线程池中已经完成运行的任何线程,然后获取锁以确保它对更新队列有独占访问权。在获取锁之后,它继续将所有排队更新刷新到 OutputWindow,然后清空队列并释放锁。

还有更多...

请参阅本主题所附的示例代码,以了解一个小型示例应用程序,该应用程序利用OutputWindow创建用于运行 ping 命令的 GUI 显示:

还有更多...

参见

  • 请参阅本章中的理解线程安全配方,以了解线程安全与 GUI 相关的讨论。

  • 请参阅本章中的使用计时器配方,以了解使用计时器的另一个示例。

第十二章:建立和管理分发应用

在本章中,我们将涵盖:

  • 使用 StandardPaths

  • 保持用户界面状态

  • 使用 SingleInstanceChecker

  • 异常处理

  • 优化针对 OS X

  • 支持国际化

  • 分发应用程序

简介

应用程序的基础设施为应用程序的内部运作提供了骨架,这些内部运作通常是用户无法直接看到的,但对于应用程序的功能至关重要。这包括诸如存储配置和外部数据文件、错误处理和安装等方面。这些领域的每一个都提供了重要的功能,并有助于提高应用程序的可用性和最终用户对应用程序的整体印象。在本章中,我们将深入探讨这些主题以及更多内容,以便为您提供适当的工具来帮助构建和分发您的应用程序。

与 StandardPaths 一起工作

几乎每个非平凡的应用都需要在程序使用之间存储数据以及加载资源,如图片。问题是把这些东西放在哪里?操作系统和用户期望在这些平台上找到这些文件的位置可能会有所不同。这个菜谱展示了如何使用 wx.StandardPaths 来管理应用程序的配置和资源文件。

如何做到这一点...

在这里,我们将创建一个薄封装实用类来帮助管理应用程序的配置文件和数据。构造函数将确保任何预定义的目录已经在系统配置存储位置设置好。

class ConfigHelper(object):
    def __init__(self, userdirs=None):
        """@keyword userdirs: list of user config
                              subdirectories names
        """
        super(ConfigHelper, self).__init__()

        # Attributes
        self.userdirs = userdirs

        # Setup
        self.InitializeConfig()

    def InitializeConfig(self):
        """Setup config directories"""
        # Create main user config directory if it does
        # not exist.
        datap = wx.StandardPaths_Get().GetUserDataDir()
        if not os.path.exists(datap):
            os.mkdir(datap)
        # Make sure that any other application specific
        # config subdirectories have been created.
        if self.userdirs:
            for dname in userdirs:
                self.CreateUserCfgDir(dname)

在这里,我们添加一个辅助函数来在当前用户的资料目录中创建一个目录:

    def CreateUserCfgDir(self, dirname):
        """Create a user config subdirectory"""
        path = wx.StandardPaths_Get().GetUserDataDir()
        path = os.path.join(path, dirname)
        if not os.path.exists(path):
            os.mkdir(path)

下一个函数可以用来获取用户数据目录中文件或目录的绝对路径:

    def GetUserConfigPath(self, relpath):
        """Get the path to a resource file
        in the users configuration directory.
        @param relpath: relative path (i.e config.cfg)
        @return: string
        """
        path = wx.StandardPaths_Get().GetUserDataDir()
        path = os.path.join(path, relpath)
        return path

最后,本类中的最后一个方法可以用来检查指定的配置文件是否已经创建:

    def HasConfigFile(self, relpath):
        """Does a given config file exist"""
        path = self.GetUserConfigPath(relpath)
        return os.path.exists(path)

它是如何工作的...

ConfigHelper 类只是对一些 StandardPaths 方法进行了一个简单的封装,以便使其使用起来更加方便。当对象被创建时,它会确保用户数据目录及其任何应用程序特定的子目录已经被创建。StandardPaths 单例使用应用程序的名称来确定用户数据目录的名称。因此,在创建 App 对象并使用 SetAppName 设置其名称之前,等待是很重要的。

class SuperFoo(wx.App):
    def OnInit(self):
        self.SetAppName("SuperFoo")
        self.config = ConfigHelper()
        self.frame = SuperFooFrame(None, title="SuperFoo")
        self.frame.Show()
        return True

    def GetConfig(self):
        return self.config

CreateUserCfgDir 提供了一种方便的方法在用户的主要配置目录内创建一个新的目录。GetUserConfigPath 可以通过使用相对于主目录的路径来获取配置目录或子目录中文件或目录的完整路径。最后,HasConfigFile 是一种简单的方法来检查用户配置文件中是否存在文件。

还有更多...

StandardPaths 单例提供了许多其他方法来获取其他系统和特定安装的安装路径。以下表格描述了这些附加方法:

方法 描述
GetConfigDir() 返回系统级配置目录
GetDataDir() 返回应用程序的全局(非用户特定)数据目录
GetDocumentsDir() 返回当前用户的文档目录
GetExecutablePath() 返回当前运行的可执行文件的路径
GetPluginsDir() 返回应用程序插件应驻留的路径
GetTempDir() 返回系统 TEMP 目录的路径
GetUserConigDir() 返回当前用户的配置目录路径

参见

  • 请参阅第九章中的创建单例配方,在设计方法和技巧中讨论了单例,例如StandardPaths对象,是什么。

  • 有关存储确认信息的更多信息,请参阅本章中的持久化 UI 状态配方。

保持用户界面状态

许多应用程序的共同特点是在程序启动之间能够记住并恢复它们的窗口大小和位置。这不是工具包提供的内置功能,因此这个配方将创建一个简单的Frame基类,该类将在应用程序使用之间自动保存和恢复其在桌面上的大小和位置。

如何做到这一点...

这个例子展示了创建一个Frame类的一种方法,该类将在程序运行之间自动恢复其位置和大小:

class PersistentFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(PersistentFrame, self).__init__(*args, **kwargs)

        # Setup
        wx.CallAfter(self.RestoreState)

        # Event Handlers
        self.Bind(wx.EVT_CLOSE, self._OnClose)

在这里,我们处理 EVT_CLOSE 事件,用于在 Frame 关闭时,将其位置和大小保存到 Config 对象中,在 Windows 上是注册表,在其他平台上是 .ini 文件:

    def _OnClose(self, event):
        position = self.GetPosition()
        size = self.GetSize()
        cfg = wx.Config()
        cfg.Write('pos', repr(position.Get()))
        cfg.Write('size', repr(size.Get()))
        event.Skip()

RestoreState 方法恢复当前存储的窗口状态,或者如果没有存储任何内容,则恢复默认状态:

    def RestoreState(self):
        """Restore the saved position and size"""
        cfg = wx.Config()
        name = self.GetName()
        position = cfg.Read(name + '.pos',
                            repr(wx.DefaultPosition))
        size = cfg.Read(name + '.size',
                        repr(wx.DefaultSize))
        # Turn strings back into tuples
        position = eval(position)
        size = eval(size)
        # Restore settings to Frame
        self.SetPosition(position)
        self.SetSize(size)

它是如何工作的...

应将PersistentFrame用作任何需要在退出时持久化其大小和位置的Frame的应用程序的基础类。这个类的工作方式相当简单,所以让我们快速了解一下它是如何工作的。

首先,为了节省其大小和位置,PersisistentFrame 将事件处理器绑定到 EVT_CLOSE。当用户关闭 Frame 时,将调用其 _OnClose 方法。在这个事件处理器中,我们简单地获取 Frame 的当前大小和位置,并将其保存到一个 wx.Config 对象中,在 Windows 上这将作为注册表,在其他平台上则是一个 .ini 文件。

相反,当创建PersistentFrame时,它会尝试从配置中读取之前保存的大小和位置。这发生在RestoreState方法中,该方法通过CallAfter来启动。这样做是为了确保我们不会在Frame创建之后恢复设置,这样如果子类设置了一些默认大小,它们就不会覆盖用户最后留下的最后状态。在RestoreState中,如果为Frame存储了信息,它将使用eval函数加载字符串并将它们转换回元组,然后简单地应用这些设置。

还有更多...

为了简化,我们只是使用了wx.Config来存储应用程序运行之间的设置。我们也可以使用StandardPaths并编写我们自己的配置文件到用户的配置目录,就像我们在之前的菜谱中所做的那样,以确保这些信息被保存在用户期望的位置。

参见

  • 请参阅本章中的 使用 StandardPaths 菜单以获取有关另一个可帮助存储和定位配置信息的类的信息。

使用 SingleInstanceChecker

有时可能希望在任何给定时间只允许一个应用程序实例存在。SingleInstanceChecker 类提供了一种检测应用程序是否已有实例正在运行的方法。这个配方创建了一个 App 类,它使用 SingleInstanceChecker 来确保计算机上一次只运行一个应用程序实例,并且还使用了一个简单的 IPC 机制,允许任何后续的应用程序实例向原始实例发送消息,告知其打开一个新窗口。

如何做到这一点...

在这里,我们将创建一个App基类,确保同一时间只运行一个进程实例,并支持一个简单的基于套接字的进程间通信机制,以通知已运行的实例有一个新的实例尝试启动:

import wx
import threading
import socket
import select

class SingleInstApp(wx.App):
    """App baseclass that only allows a single instance to
    exist at a time.
    """
    def __init__(self, *args, **kwargs):
        super(SingleInstApp, self).__init__(*args, **kwargs)

        # Setup (note this will happen after subclass OnInit)
        instid = "%s-%s" % (self.GetAppName(), wx.GetUserId())
        self._checker = wx.SingleInstanceChecker(instid)
        if self.IsOnlyInstance():
           # First instance so start IPC server
           self._ipc = IpcServer(self, instid, 27115)
           self._ipc.start()
           # Open a window
           self.DoOpenNewWindow()
        else:
            # Another instance so just send a message to
            # the instance that is already running.
            cmd = "OpenWindow.%s" % instid
            if not SendMessage(cmd, port=27115):
                print "Failed to send message!"

    def __del__(self):
        self.Cleanup()

当应用程序退出时,需要显式删除SingleInstanceChecker以确保它创建的文件锁被释放:

    def Cleanup(self):
        # Need to cleanup instance checker on exit
        if hasattr(self, '_checker'):
            del self._checker
        if hasattr(self, '_ipc'):
            self._ipc.Exit()

    def Destroy(self):
        self.Cleanup()
        super(SingleInstApp, self).Destroy()

    def IsOnlyInstance(self):
        return not self._checker.IsAnotherRunning()

    def DoOpenNewWindow(self):
        """Interface for subclass to open new window
        on ipc notification.
        """
        pass

IpcServer 类通过在机器的本地回环上打开一个套接字连接来实现进程间通信。这已被实现为一个循环等待消息的后台线程,直到收到退出指令:

class IpcServer(threading.Thread):
    """Simple IPC Server"""
    def __init__(self, app, session, port):
        super(IpcServer, self).__init__()

        # Attributes
        self.keeprunning = True
        self.app = app
        self.session = session
        self.socket = socket.socket(socket.AF_INET,
                                    socket.SOCK_STREAM)

        # Setup TCP socket
        self.socket.bind(('127.0.0.1', port))
        self.socket.listen(5)
        self.setDaemon(True)

run 方法运行服务器线程的主循环,检查套接字是否有消息,并使用 CallAfter 通知 App 调用其 DoOpenNewWindow 方法,当服务器接收到 'OpenWindow' 命令时:

    def run(self):
        """Run the server loop"""
        while self.keeprunning:
            try:
                client, addr = self.socket.accept()

                # Read from the socket
                # blocking up to 2 seconds at a time
                ready = select.select([client,],[], [],2)
                if ready[0]:
                    recieved = client.recv(4096)

                if not self.keeprunning:
                    break

                # If message ends with correct session
                # ID then process it.
                if recieved.endswith(self.session):
                    if recieved.startswith('OpenWindow'):
                        wx.CallAfter(self.app.DoOpenNewWindow)
                    else:
                        # unknown command message
                        pass
                recieved = ''
            except socket.error, msg:
                print "TCP error! %s" % msg
                break

        # Shutdown the socket
        try:
            self.socket.shutdown(socket.SHUT_RDWR)
        except:
            pass

        self.socket.close()

    def Exit(self):
        self.keeprunning = False

SendMessage 函数用于打开到 IpcServer 的套接字的客户端连接并发送指定的消息:

def SendMessage(message, port):
    """Send a message to another instance of the app"""
    try:
        # Setup the client socket
        client = socket.socket(socket.AF_INET,
                               socket.SOCK_STREAM)
        client.connect(('127.0.0.1', port))
        client.send(message)
        client.shutdown(socket.SHUT_RDWR)
        client.close()
    except Exception, msg:
        return False
    else:
        return True

与本章配套的代码中包含了一个完整的运行应用程序,展示了如何使用上述框架。为了测试它,尝试在同一台计算机上启动多个应用程序实例,并观察只有原始进程正在运行,并且每次后续启动都会在原始进程中打开一个新窗口。

它是如何工作的...

在这个菜谱中,我们在这段代码里塞进了许多内容,所以让我们来了解一下每个类是如何工作的。

SingleInstApp 类创建一个 SingleInstanceChecker 对象,以便能够检测是否有另一个应用程序实例正在运行。作为 SingleInstanceChecker 的 ID 的一部分,我们使用了用户的登录 ID,以确保实例检查器只检查同一用户启动的其他实例。

在我们的 SingleInstanceApp 对象的 __init__ 方法中,重要的是要意识到当派生类被初始化时将要发生的操作顺序。调用基类 wx.App__init__ 方法将会导致派生类的虚拟 OnInit 被调用,然后之后 SingleInstApp__init__ 方法中的其余代码将执行。如果它检测到这是应用程序的第一个运行实例,它将创建并启动我们的 IpcServer。如果不是,它将简单地创建并发送一个简单的字符串命令到另一个已经运行的 IpcServer 对象,告诉它通知其他应用程序实例创建一个新窗口。

在继续查看 IpcServer 类之前,使用 SingleInstanceChecker 时需要牢记的一个重要事项是,当你完成使用后,需要显式地删除它。如果不删除,它用于确定另一个实例是否活跃的文件锁可能永远不会释放,这可能会在程序未来的启动中引起问题。

IpcServer 类是一个简单的从 Thread 派生的类,它使用 TCP 套接字进行进程间通信。正如所述,第一个启动的 SingleInstanceApp 将创建此服务器的实例。服务器将在自己的线程中运行,检查套接字上的消息。IpcServer 线程的 run 方法只是运行一个循环,检查套接字上的新数据。如果它能够读取一条消息,它将检查消息的最后部分是否与创建 App's SingleInstanceChecker 时使用的密钥匹配,以确保命令来自应用程序的另一个实例。我们目前只为我们的简单 IPC 协议设计了支持单个 'OpenWindow' 命令,但它可以很容易地扩展以支持更多。在接收到 OpenWindow 消息后,IpcServer 将使用 CallAfter 调用 SingleInstanceApp 的接口方法 DoOpenNewWindow,通知应用程序打开其主窗口的新实例。

这个小框架的最后一部分是SendMessage函数,它被用作客户端方法来连接并向IpcServer发送消息。

参见

  • 请参阅第一章中的理解继承限制配方,在wxPython 入门中解释了 wxPython 类中重写虚拟方法的内容。

  • 查阅第十一章中的理解线程安全性配方,以获取更多关于在 wxPython GUI 中处理线程的信息,详见响应式界面

异常处理

即使在看似简单的应用中,也可能难以考虑到应用中可能发生的所有可能的错误条件。本食谱展示了如何处理未处理的异常,以及如何在应用退出之前向用户显示通知,让他们知道发生了意外的错误。

如何做到这一点...

对于这个菜谱,我们将展示如何创建一个简单的异常钩子来处理和通知用户在程序运行过程中发生的任何意外错误:

import wx
import sys
import traceback

def ExceptionHook(exctype, value, trace):
    """Handler for all unhandled exceptions
    @param exctype: Exception Type
    @param value: Error Value
    @param trace: Trace back info
    """
    # Format the traceback
    exc = traceback.format_exception(exctype, value, trace)
    ftrace = "".join(exc)
    app = wx.GetApp()
    if app:
        msg = "An unexpected error has occurred: %s" % ftrace
        wx.MessageBox(msg, app.GetAppName(),
                      style=wx.ICON_ERROR|wx.OK)
        app.Exit()
    else:
        sys.stderr.write(ftrace)

class ExceptionHandlerApp(wx.App):
    def OnInit(self):
        sys.excepthook = ExceptionHook
        return True

它是如何工作的...

这个菜谱展示了一种非常简单的方法来创建一个异常钩子,以捕获应用程序中的未处理异常。在应用程序启动期间,我们所需做的所有事情就是用我们自己的ExceptionHook函数替换默认的excepthook函数。然后,每当应用程序中抛出未处理的异常时,ExceptionHook函数就会被调用。在这个函数中,我们只是弹出一个MessageBox来显示发生了意外错误,然后告诉MainLoop退出。

还有更多...

本例的目的是展示如何优雅地处理这些错误的处理过程。因此,我们通过仅使用一个MessageBox使其保持相当简单。很容易扩展和定制这个例子,以便记录错误,或者允许用户向应用程序的开发者发送通知,以便调试错误。

优化针对 OS X

在 wxPython 应用程序中,有许多事情可以做到,以帮助它在 Macintosh OS X 系统上运行时更好地适应。用户对 OS X 上的应用程序有一些期望,这个菜谱展示了确保您的应用程序在 OS X 以及其他平台上运行良好和外观美观的一些操作。这包括标准菜单和菜单项的正确定位、主窗口的行为,以及如何启用一些 Macintosh 特定的功能。

如何做到这一点...

作为考虑一些事项的例子,我们将创建一个简单的应用程序,展示如何使应用程序符合 Macintosh UI 标准:

import wx
import sys

class OSXApp(wx.App):
    def OnInit(self):
        # Enable native spell checking and right
        # click menu for Mac TextCtrl's
        if wx.Platform == '__WXMAC__':
            spellcheck = "mac.textcontrol-use-spell-checker"
            wx.SystemOptions.SetOptionInt(spellcheck, 1)
        self.frame = OSXFrame(None,
                              title="Optimize for OSX")
        self.frame.Show()
        return True

    def MacReopenApp(self):
        self.GetTopWindow().Raise()

class OSXFrame(wx.Frame):
    """Main application window"""
    def __init__(self, *args, **kwargs):
        super(OSXFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.textctrl = wx.TextCtrl(self,
                                    style=wx.TE_MULTILINE)

        # Setup Menus
        mb = wx.MenuBar()
        fmenu = wx.Menu()
        fmenu.Append(wx.ID_OPEN)
        fmenu.Append(wx.ID_EXIT)
        mb.Append(fmenu, "&File")
        emenu = wx.Menu()
        emenu.Append(wx.ID_COPY)
        emenu.Append(wx.ID_PREFERENCES)
        mb.Append(emenu, "&Edit")
        hmenu = wx.Menu()
        hmenu.Append(wx.NewId(), "&Online Help...")
        hmenu.Append(wx.ID_ABOUT, "&About...")
        mb.Append(hmenu, "&Help")

        if wx.Platform == '__WXMAC__':
            # Make sure we don't get duplicate
            # Help menu since we used non standard name
            app = wx.GetApp()
            app.SetMacHelpMenuTitleName("&Help")

        self.SetMenuBar(mb)
        self.SetInitialSize()

if __name__ == '__main__':
    app = OSXApp(False)
    app.MainLoop()

它是如何工作的...

这个简单的应用程序创建了一个包含MenuBarTextCtrlFrame,并演示了在准备部署到 Macintosh 系统上的应用程序时需要注意的一些事项。

从我们的 OSXApp 对象的 OnInit 方法开始,我们使用了 SystemOptions 单例来启用 OS X 上 TextCtrl 对象的本地上下文菜单和拼写检查功能。此选项默认是禁用的;将其设置为 1 可以启用它。同样,在我们的 OSXApp 类中,我们重写了 MacReopenApp 方法,这是一个当应用程序的 dock 图标被点击时发生的 AppleEvent 的回调。我们重写它以确保这个点击将使我们的应用程序主窗口被带到前台,正如预期的那样。

接下来,在我们的OSXFrame类中,可以看到对于菜单部分有一些特殊处理是必要的。所有原生 OS X 应用程序在其菜单中都有一些共同元素。所有应用程序都有一个帮助菜单、一个窗口菜单和一个应用程序菜单。如果你的应用程序需要创建自定义的帮助或窗口菜单,那么需要采取一些额外的步骤来确保它们在 OS X 上能够按预期工作。在我们之前的示例中,我们创建了一个包含标题中助记符加速器的自定义帮助菜单,以便 Windows/GTK 在键盘导航中使用。由于菜单标题与默认标题不同,我们需要在App对象上调用SetMacHelpMenuTitleName,以便它知道我们的帮助菜单应该被使用。如果我们省略这一步,我们的应用程序最终会在 OS X 的MenuBar中显示两个帮助菜单。另一个需要注意的重要事项是尽可能使用库存 ID 来为菜单项设置。特别是关于“关于”、“退出”和“首选项”的条目,在 OS X 的应用程序菜单下总是会被显示。通过使用这些项目的库存 ID,wxPython 将确保它们在每个平台上都出现在正确的位置。

还有更多...

以下包含一些额外的针对 Macintosh 系统的方法和注意事项,供快速查阅。

wx.App Macintosh 特定方法

有一些其他额外的针对 Macintosh 系统的辅助方法属于App对象,可以用来自定义处理三个特殊菜单项。当应用程序在其他平台上运行时,这些方法将不会执行任何操作。

方法 描述
SetMacAboutMenuItemId 将用于识别“关于”菜单项的 ID 从ID_ABOUT更改为自定义值
SetMacExitMenuItemId 将用于识别 Exit 菜单项的 ID 从 ID_EXIT 更改为自定义值
SetMacPreferencesMenuItemId 将用于识别 Preferences 菜单项的 ID 从 ID_PREFERENCES 更改为自定义值
SetMacSupportPCMenuShortcuts 启用在 OS X 上使用菜单快捷键的功能

wx.MenuBar

可以通过使用 wx.MenuBar 的静态 SetAutoWindowMenu 方法在 OS X 上禁用 Windows 菜单的自动创建。在创建 MenuBar 之前调用 SetAutoWindowMenu 并传入 False 值将阻止 Windows 菜单的创建。

参见

  • 请参阅第一章中的使用股票 ID配方,以了解有关使用内置股票 ID 的详细讨论。

  • 请参阅第二章中的处理 Apple 事件配方,了解在 wxPython 应用程序中如何处理 AppleEvents 的示例。

  • 请参阅本章中的 分发应用程序 菜单,以了解如何在 OS X 上分发应用程序。

支持国际化

在我们今天所生活的这个互联互通的世界中,在开发应用程序界面时考虑国际化非常重要。在设计一个从一开始就完全支持国际化的应用程序时,损失非常小,但如果您不这样做,损失将会很大。这个指南将展示如何设置一个应用程序以使用 wxPython 内置的界面翻译支持。

如何做到这一点...

下面,我们将创建一个完整的示例应用程序,展示如何在 wxPython 应用程序的用户界面中支持本地化。首先要注意的是,我们下面使用的 wx.GetTranslation 的别名,用于将应用程序中所有的界面字符串包装起来:

import wx
import os

# Make a shorter alias
_ = wx.GetTranslation

接下来,在创建我们的 App 对象期间,我们创建并保存对一个 Locale 对象的引用。然后我们告诉 Locale 对象我们存放翻译文件的位置,这样它就知道在调用 GetTranslation 函数时去哪里查找翻译。

class I18NApp(wx.App):
    def OnInit(self):
        self.SetAppName("I18NTestApp")
        # Get Language from last run if set
        config = wx.Config()
        language = config.Read('lang', 'LANGUAGE_DEFAULT')

        # Setup the Locale
        self.locale = wx.Locale(getattr(wx, language))
        path = os.path.abspath("./locale") + os.path.sep
        self.locale.AddCatalogLookupPathPrefix(path)
        self.locale.AddCatalog(self.GetAppName())

        # Local is not setup so we can create things that
        # may need it to retrieve translations.
        self.frame = TestFrame(None,
                               title=_("Sample App"))
        self.frame.Show()
        return True

然后,在剩余部分,我们创建一个简单的用户界面,这将允许应用程序在英语和日语之间切换语言:

class TestFrame(wx.Frame):
    """Main application window"""
    def __init__(self, *args, **kwargs):
        super(TestFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = TestPanel(self)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize((300, 300))

class TestPanel(wx.Panel):
    def __init__(self, parent):
        super(TestPanel, self).__init__(parent)

        # Attributes
        self.closebtn = wx.Button(self, wx.ID_CLOSE)
        self.langch = wx.Choice(self,
                                choices=[_("English"),
                                         _("Japanese")])

        # Layout
        self.__DoLayout()

        # Event Handler
        self.Bind(wx.EVT_CHOICE, self.OnChoice)
        self.Bind(wx.EVT_BUTTON,
                  lambda event: self.GetParent().Close())

    def __DoLayout(self):
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)

        label = wx.StaticText(self, label=_("Hello"))
        hsizer.AddStretchSpacer()
        hsizer.Add(label, 0, wx.ALIGN_CENTER)
        hsizer.AddStretchSpacer()

        langsz = wx.BoxSizer(wx.HORIZONTAL)
        langlbl = wx.StaticText(self, label=_("Language"))
        langsz.AddStretchSpacer()
        langsz.Add(langlbl, 0, wx.ALIGN_CENTER_VERTICAL)
        langsz.Add(self.langch, 0, wx.ALL, 5)
        langsz.AddStretchSpacer()

        vsizer.AddStretchSpacer()
        vsizer.Add(hsizer, 0, wx.EXPAND)
        vsizer.Add(langsz, 0, wx.EXPAND|wx.ALL, 5)
        vsizer.Add(self.closebtn, 0, wx.ALIGN_CENTER)
        vsizer.AddStretchSpacer()

        self.SetSizer(vsizer)

    def OnChoice(self, event):
        sel = self.langch.GetSelection()
        config = wx.Config()
        if sel == 0:
            val = 'LANGUAGE_ENGLISH'
        else:
            val = 'LANGUAGE_JAPANESE'
        config.Write('lang', val)

if __name__ == '__main__':
    app = I18NApp(False)
    app.MainLoop()

它是如何工作的...

上面的简单示例展示了如何在 wxPython 应用程序中利用翻译支持。在 Choice 控件中更改选定的语言并重新启动应用程序,将改变界面字符串在英语和日语之间的转换。利用翻译相当简单,所以让我们看看使其工作的重要部分。

首先,我们为函数 wx.GetTranslation 创建了一个别名 _,这样在编写时更短,也更易于阅读。这个函数应该被包裹在应用中任何将要显示给用户的界面字符串周围。

接下来,在我们的应用程序的 OnInit 方法中,我们做了一些事情来设置适当的区域信息,以便加载配置的翻译。首先,我们创建了一个 Locale 对象。保留对这个对象的引用是必要的,以确保它不会被垃圾回收。因此,我们将它保存到 self.locale。接下来,我们设置了 Locale 对象,让它知道我们的翻译资源文件所在的位置,首先通过调用 AddCatalogLookupPathPrefix 并传入我们保存翻译文件的目录。然后,我们通过调用 AddCatalog 并传入我们的应用程序对象名称来告诉它应用程序的资源文件名称。为了加载翻译,需要在目录查找路径前缀目录下的每个语言都需要以下目录结构:

Lang_Canonical_Name/LC_MESSAGES/CatalogName.mo

因此,例如,对于我们的应用程序的日语翻译,我们在 locale 目录下有以下目录结构。

ja_JP/LC_MESSAGES/I18NTestApp.mo

在创建Locale对象之后,任何对GetTranslation的调用都将使用该区域设置从gettext目录文件中加载适当的字符串。

还有更多...

wxPython 使用 gettext 格式化 文件来加载字符串资源。对于每种翻译,都有两个文件。.po 文件(可移植对象)是用于创建默认字符串到翻译版本映射的文件,需要编辑此文件。另一个文件是 .mo 文件(机器对象),它是 .po 文件的编译版本。要将 .po 文件编译成 .mo 文件,您需要使用 msgfmt 工具。这是任何 Linux 平台上 gettext 的一部分。它也可以通过 fink 在 OS X 上安装,通过 Cygwin 在 Windows 上安装。以下命令行语句将从给定的输入 .po 文件生成 .mo 文件。

msgfmt ja_JP.po

分发应用程序

一旦你正在开发的应用程序完成,就需要准备一种方法将应用程序分发给用户。wxPython 应用程序可以像其他 Python 应用程序或脚本一样进行分发,通过创建一个setup.py脚本并使用distutils模块的setup函数。然而,这个菜谱将专注于如何通过创建一个使用py2exepy2app分别针对两个目标平台构建的构建脚本,来创建 Windows 和 OS X 的独立可执行文件。创建一个独立的应用程序使得用户在自己的系统上安装应用程序变得更加容易,这意味着更多的人可能会使用它。

准备就绪

要构建独立的可执行文件,除了 wxPython 之外,还需要一些扩展模块。因此,如果您还没有这样做,您将需要安装py2exe(Windows)或py2app(OS X)。

如何做到这一点...

在这里,我们将创建一个简单的 setup.py 模板,通过一些简单的自定义设置,可以用来构建适用于大多数 wxPython 应用程序的 Windows 和 OS X 二进制文件。顶部这里的 应用程序信息 部分可以被修改,以指定应用程序的名称和其他特定信息。

import wx
import sys

#---- Application Information ----#
APP = "FileEditor.py"
NAME = "File Editor"
VERSION = "1.0"
AUTHOR = "Author Name"
AUTHOR_EMAIL = "authorname@someplace.com"
URL = "http://fileeditor_webpage.foo"
LICENSE = "wxWidgets"
YEAR = "2010"

#---- End Application Information ----#

在这里,我们将定义一种方法,该方法使用 py2exe 从应用程序信息部分中指定的 APP 变量的 Python 脚本构建 Windows 可执行文件:

RT_MANIFEST = 24

def BuildPy2Exe():
    """Generate the Py2exe files"""
    from distutils.core import setup
    try:
        import py2exe
    except ImportError:
        print "\n!! You dont have py2exe installed. !!\n"
        exit()

Windows 的二进制文件中嵌入了一个清单,该清单指定了依赖项和其他设置。本章附带的示例代码包括以下两个 XML 文件,这些文件将确保在 Windows XP 及更高版本上运行时 GUI 具有适当的主题控件:

    pyver = sys.version_info[:2]
    if pyver == (2, 6):
        fname = "py26manifest.xml"
    elif pyver == (2, 5):
        fname = "py25manifest.xml"
    else:
        vstr = ".".join(pyver)
        assert False, "Unsupported Python Version %s" % vstr
    with open(fname, 'rb') as handle:
        manifest = handle.read()
        manifest = manifest % dict(prog=NAME)

OPTS 字典指定了 py2exe 选项。这些是一些适用于大多数应用的常规设置,但如果需要针对特定用例进行进一步调整,它们可以进行微调:

    OPTS = {"py2exe" : {"compressed" : 1,
                        "optimize" : 1,
                        "bundle_files" : 2,
                        "excludes" : ["Tkinter",],
                        "dll_excludes": ["MSVCP90.dll"]}}

setup 函数中的 windows 关键字用于指定我们正在创建一个图形用户界面应用程序,并用于指定要嵌入二进制的应用程序图标和清单:

    setup(
        name = NAME,
        version = VERSION,
        options = OPTS,
        windows = [{"script": APP,
                    "icon_resources": [(1, "Icon.ico")],
                    "other_resources" : [(RT_MANIFEST, 1,
                                          manifest)],
                  }],
        description = NAME,
        author = AUTHOR,
        author_email = AUTHOR_EMAIL,
        license = LICENSE,
        url = URL,
        )

接下来是我们的 OS X 构建方法,它使用 py2app 来构建二进制小程序包:

def BuildOSXApp():
    """Build the OSX Applet"""
    from setuptools import setup

在这里,我们定义了一个PLIST,其用途与 Windows 二进制文件使用的清单非常相似。它用于定义一些关于应用程序的信息,操作系统使用这些信息来了解应用程序扮演的角色。

    # py2app uses this to generate the plist xml for
    # the applet.
    copyright = "Copyright %s %s" % (AUTHOR, YEAR)
    appid = "com.%s.%s" % (NAME, NAME)
    PLIST = dict(CFBundleName = NAME,
             CFBundleIconFile = 'Icon.icns',
             CFBundleShortVersionString = VERSION,
             CFBundleGetInfoString = NAME + " " + VERSION,
             CFBundleExecutable = NAME,
             CFBundleIdentifier = appid,
             CFBundleTypeMIMETypes = ['text/plain',],
             CFBundleDevelopmentRegion = 'English',
             NSHumanReadableCopyright = copyright
             )

以下字典指定了setup()在构建应用程序时将使用的py2app选项:

    PY2APP_OPTS = dict(iconfile = "Icon.icns",
                       argv_emulation = True,
                       optimize = True,
                       plist = PLIST)

    setup(
        app = [APP,],
        version = VERSION,
        options = dict( py2app = PY2APP_OPTS),
        description = NAME,
        author = AUTHOR,
        author_email = AUTHOR_EMAIL,
        license = LICENSE,
        url = URL,
        setup_requires = ['py2app'],
        )

if __name__ == '__main__':
    if wx.Platform == '__WXMSW__':
        # Windows
        BuildPy2Exe()
    elif wx.Platform == '__WXMAC__':
        # OSX
        BuildOSXApp()
    else:
        print "Unsupported platform: %s" % wx.Platform

它是如何工作的...

使用之前的设置脚本,我们可以在 Windows 和 OS X 上为我们的 FileEditor 脚本构建独立的可执行文件。因此,让我们分别查看这两个函数,BuildPy2exeBuildOSXApp,看看它们是如何工作的。

BuildPy2exe 执行必要的准备工作,以便在 Windows 机器上使用 py2exe 运行 setup 来构建独立的二进制文件。在这个函数中,有三个重要的部分需要注意。首先是创建清单的部分。在 2.5 和 2.6 版本之间,用于构建 Python 解释器二进制的 Windows 运行时库发生了变化。因此,我们需要在我们的二进制清单中指定不同的依赖项,以便它能够加载正确的运行时,并给我们的 GUI 应用程序提供正确的主题外观。本主题的示例源代码中包含了 Python 2.5 或 2.6 的两种可能的清单。

第二个是py2exe选项字典。这个字典包含了在捆绑脚本时使用的py2exe特定选项。我们使用了五个选项:compressedoptimizebundle_filesexcludesdll_excludescompressed选项表示我们希望压缩生成的.exe文件。optimize选项表示要优化 Python 字节码。在这里我们可以指定012,以实现不同级别的优化。bundle_files选项指定将依赖项捆绑到library.zip文件的级别。数字越低(1-3),捆绑到 ZIP 文件中的文件数量就越多,从而减少了需要分发的单个文件的总数。使用1可能会经常导致 wxPython 应用程序出现问题,因此建议使用23。接下来,excludes选项是一个要排除在结果捆绑中的模块列表。在这里我们指定了Tkinter,只是为了确保其依赖项不会意外地被包含在我们的二进制文件中,从而使文件变大。最后,dll_excludes选项用于解决在使用py2exe与 Python 2.6 时遇到的问题。

第三点也是最后一点是setup命令中的windows参数。这个参数用于指定我们正在构建一个 GUI 应用程序,并且在这里我们指定要嵌入到.exe文件中的应用程序图标以及之前提到的清单文件。

使用 py2exe 运行 setup 与以下命令行语句一样简单:

python setup.py py2exe

现在我们来看看 py2app 的工作原理。它与 py2exe 非常相似,实际上甚至更易于使用,因为无需担心像在 Windows 上那样的运行时依赖问题。主要区别在于 PLIST,它在某种程度上类似于 Windows 上的清单文件,但用于定义一些应用程序行为并存储操作系统使用应用程序信息。Py2app 将使用指定的字典在生成的应用程序中生成 Plist XML 文件。有关可用的 Plist 选项,请参阅在 developer.apple.com 提供的适当列出的文档。PLIST 字典通过设置函数的 options 参数传递给 py2app,以及其他我们指定的 py2app 选项,例如应用程序的图标。此外,与 py2exe 非常相似,运行 py2app 只需要以下命令行语句:

python setup.py py2app

还有更多...

以下包含有关 Windows 应用程序的一些特定分布依赖性问题的附加信息,以及为 Windows 和 OS X 上的应用程序创建安装程序的参考资料。

Py2Exe 依赖项

运行 py2exe 设置命令后,请确保您审查了末尾列出的未包含的依赖项列表。可能需要手动将一些额外的文件包含到您的应用程序的 dist 文件夹中,以便在不同计算机上部署时能够正常运行。对于 Python 2.5,通常需要 msvcr71.dllgdiplus.dll 文件。对于 Python 2.6,则需要 msvcr90.dllgdiplus.dll 文件。msvcr .dll 文件由微软版权所有,因此您应该审查许可条款,以确保您有权重新分发它们。如果没有,用户可能需要单独使用可以从微软网站下载的免费可重新分发的运行时包来安装它们。

安装程序

在使用 py2exepy2app 构建你的应用程序后,你需要一种方法来帮助应用程序的用户将文件正确安装在他们的系统上。对于 Windows,有多个选项可用于构建安装程序:NSIS (nsis.sourceforge.net) 和 Inno Setup (www.jrsoftware.org/isinfo.php) 是两种流行的免费选项。在 OS X 上,必要的工具已经安装好了。只需使用磁盘工具应用程序创建一个磁盘镜像(.dmg)文件,然后将构建的应用程序复制到其中。

posted @ 2025-09-23 21:55  绝不原创的飞龙  阅读(14)  评论(0)    收藏  举报