ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 第六章 使用wxPython基本构件 1. [使用基本的建造部件](#A.2BT391KFf6Zyx2hF76kCCQ6E72-) 1. [在屏幕上绘画](#A.2BVyhcT15VTgp.2B2HU7-) 1. [如何在屏幕上绘画](#A.2BWYJPVVcoXE9eVU4Kfth1Ow-) 2. [添加窗口装饰](#A.2BbftSoHqXU.2BOIxZlw-) 1. [如何添加和更新一个状态栏](#A.2BWYJPVW37UqBUjGb0ZbBOAE4qcrZgAWgP-) 2. [如何添加菜单?](#A.2BWYJPVW37UqCD3FNV.2Fx8-) 3. [如何添加一个工具栏](#A.2BWYJPVW37UqBOAE4qXeVRd2gP-) 3. [得到标准信息](#A.2BX5dSMGgHUcZP4WBv-) 1. [如何使用标准文件对话框?](#A.2BWYJPVU9.2FdShoB1HGZYdO9lv5i91oRv8f-) 2. [如何使用标准的颜色选择器?](#A.2BWYJPVU9.2FdShoB1HGdoSYnIJykAli6VZo.2Fx8-) 4. [给应用程序一个好看的外观](#A.2BftlelHUoegtej04ATipZfXcLdoRZFonC-) 1. [如何布局窗口部件?](#A.2BWYJPVV4DXEB6l1PjkOhO9v8f-) 2. [如何建造一个关于(about)框?](#A.2BWYJPVV76kCBOAE4qUXNOjg.28about.29.2BaEb.2FHw-) 3. [如何建造一个启动画面?](#A.2BWYJPVV76kCBOAE4qVC9SqHU7l2L.2FHw-) 5. [本章小结](#A.2BZyx64FwPftM-) 即使是一个简单的`wxPython`程序也需要使用标准的元素,诸如菜单和对话框。这儿有对于任一`GUI`应用程序的基本的建造部件。使用这些建造部件,还有像启动画面、状态栏或关于框等这些窗口部件,它们给你提供了一个更友好的用户环境,并且给了你的应用程序一个专业的感观。为了要结束本书的第一部分,我们将指导你通过一个程序的创建来使用所有所学的部分。我们将建造一个简单的绘画程序,然后添加这些建造部件元素并说明使用它们时的一些问题。我们将巩固前面章节的基本原理和概念,并且最后我们将产生这个简单但专业的应用程序。本章位于先前基本概念章节和后面对`wxPython`功能更详细讨论的2、3部分之间。 我们在本章将建造的这个应用程序基本上基于`wxPython`/`samples`目录中的涂鸦例子。这个应用程序是一个非常简单的绘画程序,当鼠标左键按下时它跟踪鼠标指针,并画线。图6.1显示了一个简单的初始绘画窗口。 图6.1 ![](https://box.kancloud.cn/2016-08-21_57b9960a220e6.gif) 我们之所以要选择这样一个绘画例子,是因为它是十分简单的程序,它演示了在创建更复杂的应用程序时所引出的许多问题。在本章,我们将给你展示如何在屏幕上画线、添加状态栏、工具样以及菜单栏。你将会看到如何使用通用对话框,如文件选择器和颜色选择器。我们将使用`sizer`来布置窗口部件,并且我们也将增加一个关于框和一个启动画面。本章的最后,你将有一个很好看的绘画程序。 ## 在屏幕上绘画 你的绘画程序的首先的工作是勾画线条并显示出来。像其它的`GUI`工具一样, `wxPython`提供了一套独立于设备的工具用于绘画。下面,我们将讨论如何在屏幕上绘画。 ### 如何在屏幕上绘画 要在屏幕上绘画,我们要用到一个名为`device` `context`(设备上下文)的`wxPython`对象。设备上下文代表抽象的设备,它对于所有的设备有一套公用的绘画方法,所以对于不同的设备,你的代码是相同的,而不用考虑你所在的具体设备。设备上下文使用抽象的`wxPython`的类`wx.DC`和其子类来代表。由于`wx.DC`是抽象的,所以对于你的应用程序,你需要使用它的子类。 **使用设备上下文** 表6.1显示了`wx.DC`的子类及其用法。设备上下文用来在`wxPython`窗口部件上绘画,它应该是局部的,临时性的,不应该以实例变量、全局变量或其它形式在方法调用之间保留。在某些平台上,设备上下文是有限的资源,长期持有`wx.DC`可能导致你的程序不稳定。由于`wxPython`内部使用设备上下文的方式,对于在窗口部件中绘画,就存在几个有着细微差别的`wx.DC`的子类。第十二章将更详细地说明这些差别。 **表6.1** `wx.BufferedDC`:用于缓存一套绘画命令,直到命令完整并准备在屏幕上绘画。这防止了显示中不必要的闪烁。 `wx.BufferedPaintDC`:和`wx.BufferedDC`一样,但是只能用在一个`wx.PaintEvent`的处理中。仅临时创建该类的实例。 `wx.ClientDC`:用于在一个窗口对象上绘画。当你想在窗口部件的主区域上(不包括边框或别的装饰)绘画时使用它。主区域有时也称为客户区。`wx.ClientDC`类也应临时创建。该类仅适用于`wx.PaintEvent`的处理之外。 `wx.MemoryDC`:用于绘制图形到内存中的一个位图中,此时不被显示。然后你可以选择该位图,并使用`wx.DC.Blit()`方法来把这个位图绘画到一个窗口中。 `wx.MetafileDC`:在`Windows`操作系统上,`wx.MetafileDC`使你能够去创建标准窗口图元文件数据。 `wx.PaintDC`:等同于`wx.ClientDC`,除了它仅用于一个`wx.PaintEvent`的处理中。仅临时创建该类的实例。 `wx.PostScriptDC`:用于写压缩的`PostScript`文件。 `wx.PrinterDC`:用于`Windows`操作系统上,写到打印机。 `wx.ScreenDC`:用于直接在屏幕上绘画,在任何被显示的窗口的顶部或外部。该类只应该被临时创建。 `wx.WindowDC`:用于在一个窗口对象的整个区域上绘画,包括边框以及那些没有被包括在客户区域中的装饰。非`Windows`系统可能不支持该类。 下例6.1包含了显示图6.1的代码。因为该代码展示了基于设备上下文绘画的技巧,所以我们将对其详细注释。 例6.1 初始的`SketchWindow`代码 ``` import wx class SketchWindow(wx.Window): def __init__(self, parent, ID): wx.Window.__init__(self, parent, ID) self.SetBackgroundColour("White") self.color = "Black" self.thickness = 1 self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)#1 创建一个wx.Pen对象 self.lines = [] self.curLine = [] self.pos = (0, 0) self.InitBuffer() #2 连接事件 self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_IDLE, self.OnIdle) self.Bind(wx.EVT_PAINT, self.OnPaint) def InitBuffer(self): size = self.GetClientSize() #3 创建一个缓存的设备上下文 self.buffer = wx.EmptyBitmap(size.width, size.height) dc = wx.BufferedDC(None, self.buffer) #4 使用设备上下文 dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.Clear() self.DrawLines(dc) self.reInitBuffer = False def GetLinesData(self): return self.lines[:] def SetLinesData(self, lines): self.lines = lines[:] self.InitBuffer() self.Refresh() def OnLeftDown(self, event): self.curLine = [] self.pos = event.GetPositionTuple()#5 得到鼠标的位置 self.CaptureMouse()#6 捕获鼠标 def OnLeftUp(self, event): if self.HasCapture(): self.lines.append((self.color, self.thickness, self.curLine)) self.curLine = [] self.ReleaseMouse()#7 释放鼠标 def OnMotion(self, event): if event.Dragging() and event.LeftIsDown():#8 确定是否在拖动 dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)#9 创建另一个缓存的上下文 self.drawMotion(dc, event) event.Skip() #10 绘画到设备上下文 def drawMotion(self, dc, event): dc.SetPen(self.pen) newPos = event.GetPositionTuple() coords = self.pos + newPos self.curLine.append(coords) dc.DrawLine(*coords) self.pos = newPos def OnSize(self, event): self.reInitBuffer = True #11 处理一个resize事件 def OnIdle(self, event):#12 空闲时的处理 if self.reInitBuffer: self.InitBuffer() self.Refresh(False) def OnPaint(self, event): dc = wx.BufferedPaintDC(self, self.buffer)#13 处理一个paint(描绘)请求 #14 绘制所有的线条 def DrawLines(self, dc): for colour, thickness, line in self.lines: pen = wx.Pen(colour, thickness, wx.SOLID) dc.SetPen(pen) for coords in line: dc.DrawLine(*coords) def SetColor(self, color): self.color = color self.pen = wx.Pen(self.color, self.thickness, wx.SOLID) def SetThickness(self, num): self.thickness = num self.pen = wx.Pen(self.color, self.thickness, wx.SOLID) class SketchFrame(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, -1, "Sketch Frame", size=(800,600)) self.sketch = SketchWindow(self, -1) if __name__ == '__main__': app = wx.PySimpleApp() frame = SketchFrame(None) frame.Show(True) app.MainLoop() ``` **说明**: **#1**:`wx.Pen`实例决定绘画到设备上下文的线条的颜色、粗细和样式。样式除了`wx.SOLID`还有`wx.DOT`, `wx.LONGDASH`, 和`wx.SHORTDASH`。 **#2**:窗口需要去响应几个不同的鼠标类型事件以便绘制图形。响应的事件有鼠标左键按下和释放、鼠标移动、窗口大小变化和窗口重绘。这里也指定了空闲时的处理。 **#3**:用两步创建了缓存的设备上下文:(1)创建空的位图,它作为画面外(`offscreen)`的缓存(2)使用画面外的缓存创建一个缓存的设备上下文。这个缓存的上下文用于防止我勾画线的重绘所引起的屏幕闪烁。在这节的较后面的部分,我们将更详细地讨论这个缓存的设备上下文。 **#4**:这几行发出绘制命令到设备上下文;具体就是,设置背景色并清空设备上下文(`dc.Clear())`。必须调用`dc.Clear()`,其作用是产生一个`wx.EVT_PAINT`事件,这样,设置的背景就显示出来了,否则屏幕颜色不会改变。`wx.Brush`对象决定了背景的颜色和样式。 **#5**:事件方法`GetPositionTuple()`返回一个包含鼠标敲击的精确位置的`Python`元组。 **#6**:`CaptureMouse()`方法控制了鼠标并在窗口的内部捕获鼠标,即使是你拖动鼠标到窗口边框的外面,它仍然只响应窗口内的鼠标动作。在程序的后面必须调用`ReleaseMouse()`来取消其对鼠标的控制。否则该窗口将无法通过鼠标关闭等,试将#7注释掉。 **#7**:`ReleaseMouse()`方法将系统返回到调用`CaptureMouse()`之前的状态。`wxPython`应用程序使用一个椎栈来对捕获了鼠标的窗口的跟踪,调用`ReleaseMouse()`相当于从椎栈中弹出。这意味着你需要调用相同数据的`CaptureMouse()`和`ReleaseMouse()`。 **#8**:这行确定移动事件是否是线条绘制的一部分,由移动事件发生时鼠标左键是否处于按下状态来确定。`Dragging()`和`LeftIsDown()`都是`wx.MouseEvent`的方法,如果移动事件发生时所关联的条件成立,方法返回`true`。 **#9**:由于`wx.BufferedDC`是一个临时创建的设备上下文,所以在我们绘制线条之前需要另外创建一个。这里,我们创建一个新的`wx.ClientDC`作为主要的设备上下文,并再次使用我们的实例变量位图作为缓存。 **#10**:这几行实际是使用设备上下文去绘画新近的勾画线到屏幕上。首先,我们创建了`coords`元组,它合并了`self.pos`和`newPos`元组。这里,新的位置来自于事件`GetPositionTuple()`,老的位置是最后对`OnMotion()`调用所得到的。我们把该元组保存到`self.curLine`列表中,然后调用`DrawLine()`。*`coords`返回元组`coords`中的元素`x1`,`y1`,`x2`,`y2`。`DrawLine()`方法要求的参数形如`x1`,`y1`,`x2`,`y2`,并从点(`x1`,`y1)`到(`x2`,`y2)`绘制一条线。勾画的速度依赖于底层系统的速度。 **#11**:如果窗口大小改变了,我们存储一个`True`值到`self.reInitBuffer`实例属性中。我们实际上不做任何事直到下一个空闲事件。 **#12**:当一个空闲产生时,如果已发生了一个或多个尺寸改变事件,这个应用程序抓住时机去响应一个尺寸改变事件。我们存储一个`True`值到`self.reInitBuffer`实例属性中,并在一个空闲产生时响应的动机是避免对于接二连三的尺寸改变事件都进行屏幕刷新。 **#13**:对于所有的显示要求,都将产生`wx.EVT_PAINT`事件(描绘事件),并调用我们这里的方法`OnPaint`进行屏幕刷新(重绘),你可以看到这是出乎意料的简单:创建一个缓存的画图设备上下文。实际上`wx.PaintDC`被创建(因为我们处在一个`Paint`请求里,所以我们需要`wx.PaintDC`而非一个`wx.ClientDC`实例),然后在`dc`实例被删除后(函数返回时被销毁),位图被一块块地传送(`blit)`给屏幕并最终显示。关于缓存的更详细的信息将在随后的段落中提供。 **#14**:当由于尺寸改变(和由于从文件载入)而导致应用程序需要根据实际数据重绘线条时,被使用。这里,我们遍历存储在实例变量`self.lines`中行的列表,为每行重新创建画笔,然后根据坐标绘制每一条线。 这个例子使用了两个特殊的`wx.DC`的子类,以使用绘画缓存。一个绘画缓存是一个不显现的区域,其中存储了所有的绘画命令(这些命令能够一次被执行),并且一步到位地复制到屏幕上。缓存的好处是用户看不到单个绘画命令的发生,因此屏幕不会闪烁。正因如此,缓存被普遍地用于动画或绘制是由一些小的部分组成的场合。 在`wxPython`中,有两个用于缓存的类:`wx.BufferDC`(通常用于缓存一个`wx.ClientDC`)、`wx.BufferPaintDC`(用于缓存一个`wx.PaintDC`)。它们工作方式基本上一样。缓存设备上下文的创建要使用两个参数。第一个是适当类型的目标设备上下文(例如,在例6.1中的#9,它是一个新的`wx.ClientDC`实例)。第二个是一个`wx.Bitmap`对象。在例6.1中,我们使用函数`wx.EmptyBitmap`创建一个位图。当绘画命令到缓存的设备上下文时,一个内在的`wx.MemoryDC`被用于位图绘制。当缓存对象被销毁时,C++销毁器使用`Blit()`方法去自动复制位图到目标。在`wxPython`中,销毁通常发生在对象退出作用域时。这意味缓存的设备上下文仅在临时创建时有用,所以它们能够被销毁并能用于块传送(`blit)`。 例如例6.1的`OnPaint`()方法中,`self.buffer`位图在建造勾画(`sketch`)期间已经被写了。只需要创建缓存对象,从而建立关于窗口的已有的位图与临时`wx.PaintDC()`之间的连接。方法结束后,缓存`DC`立即退出作用域,触发它的销毁器,同时将位图复制到屏幕。 **设备上下文的函数** 当你使用设备上下文时,要记住根据你的绘制类型去使用恰当的上下文(特别要记住`wx.PaintDC`和 `wx.ClientDC`的区别)。一旦你有了适当的设备上下文,然后你就可以用它们来做一些事情了。表6.2列出 了`wx.DC`的一些方法。 **表6.2** **`wx.DC`的常用方法** `Blit(xdest`, `ydest`, `width`,`height`, `source`, `xsrc`,`ysrc)`:从源设备上下文复制块到调用该方法的设备上下文。参数`xdest`, `ydest`是复制到目标上下文的起始点。接下来的两个参数指定了要复制的区域的宽度和高度。`source`是源设备上下文,`xsrc`,`ysrc`是源设备上下文中开始复制的起点。还有一些可选的参数来指定逻辑叠加功能和掩码。 `Clear()`:通过使用当前的背景刷来清除设备上下文。 `DrawArc(x1`, `y1`, `x2`, `y2`,`xc`, `yc)`:使用起点(`x1`, `y1)`和终点(`x2`, `y2)`画一个圆弧。(`xc`, `yc)`是圆弧的中心。圆弧使用当前的画刷填充。这个函数按逆时针画。这也有一个相关的方法`DrawEllipticalArc()`。 `DrawBitmap(bitmap`, x,y, `transparent)`:绘制一个`wx.Bitmap`对象,起点为(x, `y)`。如果`transparent`为真,所复制的位图将是透明的。 `DrawCircle(x`, y, `radius)` `DrawCircle(point`, `radius)`:按给定的中心点和半径画圆。这也有一个相关的方法`DrawEllipse`。 `DrawIcon(icon`, x, `y)`:绘制一个`wx.Icon`对象到上下文,起点是(x, `y)`。 `DrawLine(x1`, `y1`, `x2`, `y2)`:从点(`x1`, `y1)`到(`x2`, `y2)`画一条线。这有一个相关的方法`DrawLines()`,该方法要`wx.Point`对象的一个`Python`列表为参数,并将其中的点连接起来。 `DrawPolygon(points)`:按给定的`wx.Point`对象的一个`Python`列表绘制一个多边形。与`DrawLines()`不同的是,它的终点和起点相连。多边形使用当前的画刷来填充。这有一些可选的参数来设置x和y的偏移以及填充样式。 `DrawRectangle(x`, y,`width`, `height)`:绘制一个矩形,它的左上角是(x, `y)`,其宽和高是`width`和`height` 。 `DrawText(text`, x, `y)`:从点(x, `y)`开始绘制给定的字符串,使用当前的字体。相关函数包括`DrawRotatedText()`和`GetTextExtent()`。文本项有前景色和背景色属性。 `FloodFill(x`, y, `color`,`style)`:从点(x, `y)`执行一个区域填充,使用当前画刷的颜色。参数`style`是可选的。`style`的默认值是`wx.FLOOD_SURFACE`,它表示当填充碰到另一颜色时停止。另一值`wx.FLOOD_BORDER`表示参数`color`是填充的边界,当填充碰到该颜色的代表的边界时停止。 `GetBackground()` `SetBackground(brush)`:背景画刷是一个`wx.Brush`对象,当`Clear()`方法被调用时使用。 `GetBrush()` `SetBrush(brush)`:画刷是一个`wx.Brush`对象并且用于填充任何绘制在设备上下文上的形状。 `GetFont()` `SetFont(font)`:字体(`font)`是一个`wx.Font`对象,被用于所有的文本绘制操作。 `GetPen()` `SetPen(pen)`:画笔(`pen)`是一个`wx.Pen`对象,被用于所有绘制线条的操作。 `GetPixel(x`, `y)`:返回一个关于点(x, `y)`的像素的一个`wx.Colour`对象。 `GetSize()` `GetSizeTuple()`:以一个`wx.Size`对象或一个`Python`元组的形式返回设备上下文的像素尺寸。 上面的列表并没有囊括所有的方法。另外的一些方法将在第十二章中说明。 ## 添加窗口装饰 尽管绘制到屏幕是一个画图程序不可或缺的部分,但是它距美观的程序还差的远。在这一节,我们将谈及常用的窗口装饰:状态栏、菜单和工具栏。我们将在第10章对这些做更详细的讨论。 ### 如何添加和更新一个状态栏 在`wxPython`中,你可以通过调用框架的`CreateStatusBar()`方法添加并放置一个状态栏到一个框架的底部。当父框架调整大小的时候,状态栏自动的自我调整大小。默认情况下,状态栏是类`wx.StatusBar`的一个实例。要创建一个自定义的状态栏,要使用`SetStatusBar()`方法并要求你的新类的实例作为参数来将状态栏附着到你的框架上。 要在你的状态栏上显示单一的一段文本,你可以使用`wx.StatusBar`的`SetStatusText()`方法。例6.2扩展了在例6.1中所演示的`SketchFrame`类来在状态栏中显示当前鼠标的位置。 例6.2 给框架添加一个简单的状态栏 ``` # python import wx from example1 import SketchWindow class SketchFrame(wx.Frame): def init(self, parent): wx.Frame.init(self, parent, -1, "Sketch Frame", size=(800,600)) self.sketch = SketchWindow(self, -1) self.sketch.Bind(wx.EVT_MOTION, self.OnSketchMotion) self.statusbar = self.CreateStatusBar() def OnSketchMotion(self, event): self.statusbar.SetStatusText(str(event.GetPositionTuple())) event.Skip() if name == 'main': app = wx.PySimpleApp() frame = SketchFrame(None) frame.Show(True) app.MainLoop() ``` 我们通过使框架捕捉勾画窗的`wx.EVT_MOTION`事件来在状态栏中显示鼠标位置。事件处理器使用由该事件提供的数据设置状态栏的文?尽H缓蟮饔脅方法来保证另外的`OnMotion()`方法被调用,否则线条将不被绘制。 如果你想在状态栏中显示多个文本元素,你可以在状态栏中创建多个文本域。要使用这个功能,你要调用`SetFieldsCount()`方法,其参数是域的数量;默认情况下只有我们先前所见的那一个域。这之后使用先前的`SetStatusText()`,但是要使用第二个参数来指定此方法所应的域。域的编号从0开始。如果你不指定一个域,那么默认为设置第0号域,这也说明了为什么我们没有指定域而先前的例子能工作。 默认情况下,每个域的宽度是相同的。要调整文本域的尺寸,`wxPython`提供了`SetStatusWidth()`方法。该方法要求一个整数的`Python`列表作为参数,列表的长度必须和状态栏中哉的数量一致。按列表中整数的顺序来计算对应域的宽度。如果整数是正值,那么宽度是固定的。如果你想域的宽度随框架的变化而变化,那么应该使用负值。负值的绝对值代表域的相对宽度;可以把它认为是所占总宽度的比例。例如调用`statusbar.SetStatusWidth(`[-1, -2,-3])方法所导致的各域从左到右的宽度比例是1:2:3。图6.2显示了这个结果。 图6.2 ![](https://box.kancloud.cn/2016-08-21_57b9960a343fe.gif) 例子6.3增加了两个状态域,其中一个显示所绘的当前线条的点数,另一个显示当前所画的线条的数量。该例所产生的状态条如图6.2所示。 例6.3 支持多个状态域 ``` import wx from example1 import SketchWindow class SketchFrame(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, -1, "Sketch Frame", size=(800,600)) self.sketch = SketchWindow(self, -1) self.sketch.Bind(wx.EVT_MOTION, self.OnSketchMotion) self.statusbar = self.CreateStatusBar() self.statusbar.SetFieldsCount(3) self.statusbar.SetStatusWidths([-1, -2, -3]) def OnSketchMotion(self, event): self.statusbar.SetStatusText("Pos: %s" % str(event.GetPositionTuple()), 0) self.statusbar.SetStatusText("Current Pts: %s" % len(self.sketch.curLine), 1) self.statusbar.SetStatusText("Line Count: %s" % len(self.sketch.lines), 2) event.Skip() if __name__ == '__main__': app = wx.PySimpleApp() frame = SketchFrame(None) frame.Show(True) app.MainLoop() ``` `StatusBar`类使你能够把状态域当作一个后进先出的堆栈。尽管本章的演示程序没有这样用,`PushStatus`- `Text()`和`PopStatusText()`使得你能够在临时显示新的文本之后返回先前的状态文本。这两个方法都有一个可选的域号参数,以便在多个状态域的情况下使用。 表6.3归纳了`wx.StatusBar`最常用的方法 **表6.3** **`wx.StatusBar`的方法** `GetFieldsCount()` `SetFieldsCount(count)`:得到或设置状态栏中域的数量。 `GetStatusText(field`=0) `SetStatusText(text`, `field`=0):得到或设置指定域中的文本。0是默认值,代表最左端的域。 `PopStatusText(field`=0):弹出堆栈中的文本到指定域中,以改变域中的文本为弹出值。 `PushStatusText(text`, `field`=0):改变指定的域中的文本为给定的文本,并将改变前的文本压入堆栈的顶部。 `SetStatusWidths(widths)`:指定各状态域的宽度。`widths`是一个整数的`Python`列表。 在第10章中,我们将对状态栏作更详细的说明。下面我们将讨论菜单。 ### 如何添加菜单? 本节,我们将说明如何添加子菜单和复选或单选菜单。子菜单是顶级菜单中的菜单。复制菜单或单选菜单是一组菜单项,它们的行为类似于一组复选框或单选按钮。图6.3显示了一个菜单栏,其中的一个子菜单包含了单选菜单项。 图6.3 ![](https://box.kancloud.cn/2016-08-21_57b9960a476bd.gif) 要创建一个子菜单,首先和创建别的菜单方法一样创建一个菜单,然后再使用`wx.Menu.AppendMenu()`将它添加给父菜单。 带有复选或单选菜单的菜单可以通过使用`wx.Menu`的`AppendCheckItem()`和`AppendRadioItem()`方法来创建,或通过在`wx.MenuItem`的创建器中使参数`kind`的属性值为下列之一来创建:`wx.ITEM_NORMAL`, `wx.ITEM_CHECKBOX`, 或 `wx.ITEM_RADIO`。要使用编程的方法来选择一个菜单项,可以使`wx.Menu`的`Check(id`,`bool)`方法,`id`是所要改变项的`wxPython` `ID`,`bool`指定了该项的选择状态。 例6.4为我们初始的绘画程序添加了菜单支持。我们这里的菜单改进自例5.5中的被重构的公用程序代码。 **例子6.4** ``` import wx from example1 import SketchWindow class SketchFrame(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, -1, "Sketch Frame", size=(800,600)) self.sketch = SketchWindow(self, -1) self.sketch.Bind(wx.EVT_MOTION, self.OnSketchMotion) self.initStatusBar() #1 这里因重构有点变化 self.createMenuBar() def initStatusBar(self): self.statusbar = self.CreateStatusBar() self.statusbar.SetFieldsCount(3) self.statusbar.SetStatusWidths([-1, -2, -3]) def OnSketchMotion(self, event): self.statusbar.SetStatusText("Pos: %s" % str(event.GetPositionTuple()), 0) self.statusbar.SetStatusText("Current Pts: %s" % len(self.sketch.curLine), 1) self.statusbar.SetStatusText("Line Count: %s" % len(self.sketch.lines), 2) event.Skip() def menuData(self): #2 菜单数据 return [(" ", ( (" ", "New Sketch file", self.OnNew), (" ", "Open sketch file", self.OnOpen), (" ", "Save sketch file", self.OnSave), ("", "", ""), (" ", ( (" ", "", self.OnColor, wx.ITEM_RADIO), (" ", "", self.OnColor, wx.ITEM_RADIO), (" ", "", self.OnColor, wx.ITEM_RADIO), (" ", "", self.OnColor, wx.ITEM_RADIO))), ("", "", ""), (" ", "Quit", self.OnCloseWindow)))] def createMenuBar(self): menuBar = wx.MenuBar() for eachMenuData in self.menuData(): menuLabel = eachMenuData[0] menuItems = eachMenuData[1] menuBar.Append(self.createMenu(menuItems), menuLabel) self.SetMenuBar(menuBar) def createMenu(self, menuData): menu = wx.Menu() #3 创建子菜单 for eachItem in menuData: if len(eachItem) == 2: label = eachItem[0] subMenu = self.createMenu(eachItem[1]) menu.AppendMenu(wx.NewId(), label, subMenu) else: self.createMenuItem(menu, *eachItem) return menu def createMenuItem(self, menu, label, status, handler, kind=wx.ITEM_NORMAL): if not label: menu.AppendSeparator() return menuItem = menu.Append(-1, label, status, kind)#4 使用kind创建菜单项 self.Bind(wx.EVT_MENU, handler, menuItem) def OnNew(self, event): pass def OnOpen(self, event): pass def OnSave(self, event): pass def OnColor(self, event):#5 处理颜色的改变 menubar = self.GetMenuBar() itemId = event.GetId() item = menubar.FindItemById(itemId) color = item.GetLabel() self.sketch.SetColor(color) def OnCloseWindow(self, event): self.Destroy() if __name__ == '__main__': app = wx.PySimpleApp() frame = SketchFrame(None) frame.Show(True) app.MainLoop() ``` **说明**: **#1**:现在`__init__`方法包含了更多的功能,我们把状态栏放在了它自己的方法中。 **#2**:菜单数据的格式现在是(标签, (项目)),其中的每个顶目也是一个列表(标签, 描术文字, 处理器, 可选的`kind)`或一个带有标签和项目的菜单。确定数据的一个子项目是菜单还是一个菜单项,请记住,菜单的长度是2,项目的长度是3或4。对于更复杂的产品数据,我建议使用`XML`或别的外部格式。 **#3**:如果数据块的长度是2,这意味它是一个菜单,将之分开,并递归调用`createMenu`,然后将之添加。 **#4**:创建菜单项。对`wx.MenuItem`的构造器使用`kind`参数的方法比使用`wx.Menu`的特定方法更容易。 **#5**:`OnColor`方法根据所选菜单项来改变画笔的颜色。代码根据事件得到项目的`id`,再使用`FindItemById()`来得到正确的菜单项(注意我们这里使用菜单栏作为数据结构来访问,而没有使用项目`id`的哈希表),这个方法是以标签是`wxPython`颜色名为前提的。 ### 如何添加一个工具栏 菜单栏和工具栏通常是紧密联系在一起的,工具栏的绝大部分功能与菜单项相对应。在`wxPython`中,这通过工具栏被敲击时发出`wx.EVT_MENU`事件,这样就可很容易地在处理菜单项的选择和工具栏的敲击时使用相同的方法。一个`wxPython`工具栏是类`wx.ToolBar`的一个实例,正如我们在第二章中所见的,可以使用框架的方法`CreateToolBar()`来创建。和状态栏一样,工具栏的大小也随其父框架的改变而自动改变。工具栏与其它的`wxPython`窗口一样可以拥有任意的子窗口。工具栏也包含创建工具按钮的方法。图6.4显示了带有一个工具栏的`sketch`窗口的一部分,这个工具栏使用了6.2.2中菜单的相应方法,以与菜单项的功能相对应。 图6.4 ![](https://box.kancloud.cn/2016-08-21_57b9960a5bfd6.gif) 例6.5中,我们没有重复使用菜单改变画笔颜色的代码,而是使用了新的方法。 **例6.5** **添加一个工具栏到`sketch`应用程序** ``` def __init__(self, parent): wx.Frame.__init__(self, parent, -1, "Sketch Frame",size=(800,600)) self.sketch = SketchWindow(self, -1) self.sketch.Bind(wx.EVT_MOTION, self.OnSketchMotion) self.initStatusBar() self.createMenuBar() self.createToolBar() def createToolBar(self):#1创建工具栏 toolbar = self.CreateToolBar() for each in self.toolbarData(): self.createSimpleTool(toolbar, *each) toolbar.AddSeparator() for each in self.toolbarColorData(): self.createColorTool(toolbar, each) toolbar.Realize()#2 显现工具栏 def createSimpleTool(self, toolbar, label, filename,help, handler):#3 创建常规工具 if not label: toolbar.AddSeparator() return bmp = wx.Image(filename,wx.BITMAP_TYPE_BMP).ConvertToBitmap() tool = toolbar.AddSimpleTool(-1, bmp, label, help) self.Bind(wx.EVT_MENU, handler, tool) def toolbarData(self): return (("New", "new.bmp", "Create new sketch",self.OnNew), ("", "", "", ""), ("Open", "open.bmp", "Open existing sketch",self.OnOpen), ("Save", "save.bmp", "Save existing sketch",self.OnSave)) def createColorTool(self, toolbar, color):#4 创建颜色工具 bmp = self.MakeBitmap(color) newId = wx.NewId() tool = toolbar.AddRadioTool(-1, bmp, shortHelp=color) self.Bind(wx.EVT_MENU, self.OnColor, tool) def MakeBitmap(self, color):#5 创建纯色的位图 bmp = wx.EmptyBitmap(16, 15) dc = wx.MemoryDC() dc.SelectObject(bmp) dc.SetBackground(wx.Brush(color)) dc.Clear() dc.SelectObject(wx.NullBitmap) return bmp def toolbarColorData(self): return ("Black", "Red", "Green", "Blue") def OnColor(self, event):#6 改变画笔颜色以响应工具栏的敲击 menubar = self.GetMenuBar() itemId = event.GetId() item = menubar.FindItemById(itemId) if not item: toolbar = self.GetToolBar() item = toolbar.FindById(itemId) color = item.GetShortHelp() else: color = item.GetLabel() self.sketch.SetColor(color) ``` **#1**:工具栏的代码在设置上类似于菜单代码。然而,这里,我们对常规的按钮和单选切换按钮使用了不同的循环设置。 **#2**:`Realize()`方法实际上是在工具栏中布局工具栏对象。它在工具栏被显示前必须被调用,如果工具栏中的添加或删除了工具,那么这个方法也必须被调用。 **#3**:这个方法类似于菜单项的创建。主要区别是工具栏上的工具要求显示位图。这里我们使用了三个位于同一目录下基本位图。在该方法的最后,我们绑定了菜单项所使用的相同的`wx.EVT_MENU`事件。 **#4**:颜色工具的创建类似于常规的工具。唯一的不同是使用了一个不同的方法去告诉工具栏它们是单选工具。纯色的位图由`MakeBitmap()`方法创建。 **#5**:该方法为单选工具创建纯色的位图。 **#6**:该方法在原有的基础上添加了搜索正确的工具以具此来改变颜色。然而,所写的代码的问题是,通过菜单项使画笔颜色改变了,但是工具栏上的单选工具的状态没有相应改变,反过来也是一样。 工具栏中的工具在鼠标右键敲击时能够产生`wx.EVT_TOOL_RCLICKED`类型事件。工具栏也有一些不同的样式,它们被作为位图参数传递给`CreateToolBar()`。表6.4列出了一些工具栏的样式。 **表6.4** **`wx.ToolBar`类的样式** `wx.TB_3DBUTTONS`:3D外观 `wx.TB_HORIZONTAL`:默认样式,工具栏水平布置 `wx.TB_NOICONS`:不为每个工具显示位图 `wx.TB_TEXT`:根据不同的位图显示简短的帮助文本 `wx.TB_VERTICAL`:垂直放置工具栏 工具栏比状态栏更复杂。表6.5显示了其常用的一些方法。 **表6.5** **`wx.ToolBar`的常用方法** `AddControl(control)`:添加一个任意的`wxPython`控件到工具栏。相关方法`InsertControl()`。 `AddSeparator()`:在工具之间放置空格。 `AddSimpleTool(id`, `bitmap`,`shortHelpString`="",`kind`=`wx.ITEM_NORMAL)`:添加一个简单的带有给定位图的工具到工具栏。`shortHelpString`作为提示显示。`kind`的值可以是`wx.ITEM_NORMAL`, `wx.ITEM_CHECKBOX`, 或`wx.ITEM_RADIO`。 `AddTool(id`, `bitmap`,`bitmap2`=`wx.NullBitmap`,`kind`=`wx.ITEM_NORMAL`,`shortHelpString`="",`longHelpString`="", `clientData`=`None)`:简单工具的其它参数。`bitmap2`是当该工具被按下时所显示的位图。`longHelpString`是当指针位于该工具中时显示在状态栏中的帮助字符串。`clientData`用于将任意的一块数据与工具相联系起来。相关方法`InsertTool()`。 `AddCheckTool(...)`:添加一个复选框切换工具,所要求参数同`AddTool()`。 `AddRadioTool(...)`:添加一个单选切换工具,所要求参数同`AddTool()`。对于连续的未分隔的单选工具被视为一组。 `DeleteTool(toolId)` `DeleteToolByPosition(x`, `y)`:删除所给定的`id`的工具,或删除给定显示位置的工具。 `FindControl(toolId)` `FindToolForPosition(x`, `y)`:查找并返回给定`id`或显示位置的工具。 `ToggleTool(toolId`, `toggle)`:根据布尔什`toggle`来设置给定`id`的工具的状态。 下一节,我们将给你展示如何使用通用对话框来得到用户的信息。 ## 得到标准信息 你的应用程序经常需要从用户那里得到基本的信息,这通常通过对话框。在这一节,我们将讨论针对标准用户信息的标准文件和颜色对话框。 ### 如何使用标准文件对话框? 大部分的`GUI`应用程序都要保存和载入这样那样的数据。考虑到你和你的用户,应该有一个简单的,方便的机制来选择文件。很高兴,为此`wxPython`提供了标准的文件对话框`wx.FileDialog`。图6.5显示了这个用于`sketch`程序的文件对话框。 ![](https://box.kancloud.cn/2016-08-21_57b9960a70a83.gif) `wx.FileDialog`最重要的方法是它的构造器,语法如下: `wx.FileDialog(parent`, `message`="`Choose` a `file`", `defaultDir`="", `defaultFile`="", `wildcard`="*.*", `style`=0) 表6.6对构造器的参数进行了说明。 **表6.6** **`wx.FileDialog`构造器的参数** `parent`:对话框的父窗口。如果没有父窗口则为`None`。 `message`:`message`显示在对话框的标题栏中。 `defaultDir`:当对话框打开时,默认的目录。如果为空,则为当前工作目录。 `defaultFile`:当对话框打开时,默认选择的文件。如果为空,则没有文件被选择。 `wildcard`:通配符。指定要选择的文件类型。格式是 `display` | `wildcard` 。可以指定多种类型的文件,例如:“`Sketch` `files` (*.`sketch)`|*.`sketch`|`All` `files` (*.*)|*.*”。 `style`:样式。见下表6.7。 **表6.7** **`wx.FileDialog`的样式** `wx.CHANGE_DIR`:在用户选择了一个文件之后,当前工作目录相应改变到所选文件所在的目录。 `wx.MULTIPLE`:仅适用于打开对话框。这个样式使得用户可以选择多个文件。 `wx.OPEN`:这个样式用于打开一个文件。 `wx.OVERWRITE_PROMPT`:仅适用于保存文件对话框。显示一个提示信息以确认是否覆盖一个已存在的文件。 `wx.SAVE`:这个样式用于保存一个文件。 要使用文件对话框,要对一个对话框实例调用`ShowModal()`方法。这个方法根据用户所敲击的对话框上的按钮来返回`wx.ID_OK`或`wx.ID_CANCEL`。选择之后。使用`GetFilename()`, `GetDirectory()`, 或`GetPath()`方法来获取数据。之后,调用`Destroy()`销毁对话框是一个好的观念。 下例6.6显了对`SketchFrame`所作的修改以提供保存和装载(完整的附书源码请到论坛的"相关资源"的"教程下载"中下载)。这些改变要求导入`cPickle`和`os`模块。我们使用`cPickle`来将数据的列表转换为可用于文件读写的数据格式。 **例6.6** **`SketchFrame`的保存和装载方法** ``` def __init__(self, parent): self.title = "Sketch Frame" wx.Frame.__init__(self, parent, -1, self.title,size=(800,600)) self.filename = "" self.sketch = SketchWindow(self, -1) self.sketch.Bind(wx.EVT_MOTION, self.OnSketchMotion) self.initStatusBar() self.createMenuBar() self.createToolBar() def SaveFile(self):#1 保存文件 if self.filename: data = self.sketch.GetLinesData() f = open(self.filename, 'w') cPickle.dump(data, f) f.close() def ReadFile(self):#2 读文件 if self.filename: try: f = open(self.filename, 'r') data = cPickle.load(f) f.close() self.sketch.SetLinesData(data) except cPickle.UnpicklingError: wx.MessageBox("%s is not a sketch file." % self.filename, "oops!", style=wx.OK|wx.ICON_EXCLAMATION) wildcard = "Sketch files (*.sketch)|*.sketch|All files (*.*)|*.*" def OnOpen(self, event):#3 弹出打开对话框 dlg = wx.FileDialog(self, "Open sketch file...",os.getcwd(), style=wx.OPEN,wildcard=self.wildcard) if dlg.ShowModal() == wx.ID_OK: self.filename = dlg.GetPath() self.ReadFile() self.SetTitle(self.title + ' -- ' + self.filename) dlg.Destroy() def OnSave(self, event):#4 保存文件 if not self.filename: self.OnSaveAs(event) else: self.SaveFile() def OnSaveAs(self, event):#5 弹出保存对话框 dlg = wx.FileDialog(self, "Save sketch as...", os.getcwd(), style=wx.SAVE | wx.OVERWRITE_PROMPT, wildcard=self.wildcard) if dlg.ShowModal() == wx.ID_OK: filename = dlg.GetPath() if not os.path.splitext(filename)[1]:#6 确保文件名后缀 filename = filename + '.sketch' self.filename = filename self.SaveFile() self.SetTitle(self.title + ' -- ' + self.filename) dlg.Destroy() ``` **#1**:该方法写文件数据到磁盘中,给定了文件名,使用了`cPickle`模块。 **#2**:该方法使用`cPickle`来读文件。如果文件不是期望的类型,则弹出一个消息框来警告。 **#3**:`OnOpen()`方法使用`wx.OPEN`样式来创建一个对话框。通配符让用户可以限定选择.`sketch`文件。如果用户敲击`OK`,那么该方法根据所选择的路径调用`ReadFile()`方法。 **#4**:如果已经选择了用于保存当前数据的文件名,那么保存文件,否则,我们打开保存对话框。 **#5**:`OnSave()`方法创建一个`wx.SAVE`文件对话框。 **#6**:这行确保文件名后缀为.`sketch`。 下一节,我们将讨论如何使用文件选择器。 ### 如何使用标准的颜色选择器? 如果用户能够在`sketch`对话框中选择任意的颜色,那么这将是有用。对于这个目的,我们可以使用`wxPython`提供的标准`wx.ColourDialog`。这个对话框的用法类似于文件对话框。它的构造器只需要一个`parent(`双亲)和一个可选的数据属性参数。数据属性是一个`wx.ColourData`的实例,它存储与该对话框相关的一些数据,如用户选择的颜色,还有自定义的颜色的列表。使用数据属性使你能够在以后的应用中保持自定义颜色的一致性。 在`sketch`应用程序中使用颜色对话框,要求增加一个菜单项和一个处理器方法。例6.7显示了所增加的代码。 **例6.7** **对`SketchFrame`做一些改变,以显示颜色对话框** ``` def menuData(self): return [(" ", ( (" ", "New Sketch file", self.OnNew), (" ", "Open sketch file", self.OnOpen), (" ", "Save sketch file", self.OnSave), ("", "", ""), (" ", ( (" ", "", self.OnColor,wx.ITEM_RADIO), (" ", "", self.OnColor,wx.ITEM_RADIO), (" ", "", self.OnColor,wx.ITEM_RADIO), (" ", "", self.OnColor,wx.ITEM_RADIO), (" ", "", self.OnOtherColor,wx.ITEM_RADIO))), ("", "", ""), (" ", "Quit", self.OnCloseWindow)))] def OnOtherColor(self, event): dlg = wx.ColourDialog(self) dlg.GetColourData().SetChooseFull(True)# 创建颜色数据对象 if dlg.ShowModal() == wx.ID_OK: self.sketch.SetColor(dlg.GetColourData().GetColour())# 根据用户的输入设置颜色 dlg.Destroy() ``` 颜色数据实例的`SetChooseFull()`方法告诉对话框去显示整个调色板,其中包括了自定义的颜色信息。对话框关闭后,我们根据得到的颜色来拾取颜色数据。颜色数据作为一个`wx.Color`的实例返回并传递给`sketch`程序来设置颜色。 ## 给应用程序一个好看的外观 在这一节中,我们将讨论如何让你的程序有一个好的外观。从重要性来说,储如你如何作安排以便用户调整窗口的大小,从细节来说,储如你如何显示一个`about`框。在本书的第二部分,我们将更详细地对这些主题进行进一步的讨论。 ### 如何布局窗口部件? 在你的`wxPython`应用程序中布局你的窗口部件的方法之一是,在每个窗口部件被创建时显式地指定它的位置和大小。虽然这个方法相当地简单,但是它一直存在几个缺点。其中之一就是,因为窗口部件的尺寸和默认字体的尺寸不同,对于在所有系统上要得到一个正确的定位是非常困难的。另外,每当用户调整父窗口的大小时,你必须显式地改变每个窗口部件的定位,要正确地实现它是十分痛苦的。 幸运的是,这儿有一个更好的方法。在`wxPython`中的布局机制是一个被称为`sizer`的东西,它类似于`Java` `AWT`和其它的界面工具包中的布局管理器。每个不同的`sizer`基于一套规则管理它的窗口的尺寸和位置。`sizer`属于一个容器窗口(比如`wx.Panel`)。在父中创建的子窗口必须被添加给`sizer`,`sizer`管理每个窗口部件的尺寸和位置。 **创建一个`sizer`** 创建一个`sizer`的步骤: 1、创建你想用来自动调用尺寸的`panel`或`container(`容器)。 2、创建`sizer`。 3、创建你的子窗口。 4、使用`sizer`的`Add()`方法来将每个子窗口添加给`sizer`。当你添加窗口时,给了`sizer`附加的信息,这包括窗口周围空间的度量、在由`sizer`所管理分配的空间中如何对齐窗口、当容器窗口改变大小时如何扩展子窗口等。 5、`sizer`可以嵌套,这意味你可以像窗口对象一样添加别的`sizer`到父`sizer`。你也可以预留一定数量的空间作为分隔。 6、调用容器的`SetSizer(sizer)`方法。 表6.8列出了在`wxPython`中有效的最常用的`sizer`。对于每个专门的`sizer`的更完整的说明见第11章。 **表6.8** **最常用的`wxPython`的`sizer`** `wx.BoxSizer`:在一条线上布局子窗口部件。`wx.BoxSizer`的布局方向可以是水平或坚直的,并且可以在水平或坚直方向上包含子`sizer`以创建复杂的布局。在项目被添加时传递给`sizer`的参数控制子窗口部件如何根据`box`的主体或垂直轴线作相应的尺寸调整。 `wx.FlexGridSizer`:一个固定的二维网格,它与`wx.GridSizer`的区别是,行和列根据所在行或列的最大元素分别被设置。 `wx.GridSizer`:一个固定的二维网格,其中的每个元素都有相同的尺寸。当创建一个`grid` `sizer`时,你要么固定行的数量,要么固定列的数量。项目被从左到右的添加,直到一行被填满,然后从下一行开始。 `wx.GridBagSizer`:一个固定的二维网格,基于`wx.FlexGridSizer`。允许项目被放置在网格上的特定点,也允许项目跨越多和网格区域。 `wx.StaticBoxSizer`:等同于`wx.BoxSizer`,只是在`box`周围多了一个附加的边框(有一个可选的标签)。 **使用`sizer`** 为了演示`sizer`的用法,我们将给`sketch`应用程序增加一个`control` `panel`。`control` `panel`包含用来设置线条颜色和粗细的按钮。这个例子使用了`wx.GridSizer`(用于按钮)和`wx.BoxSizer`(用于其余的布局部分)。图6.6显示了使用了`panel`的`sketch`应用程序,并图解了`grid`和`box`的实际布局。 ![](https://box.kancloud.cn/2016-08-21_57b9960a8b316.gif) 例6.8显示了实现`control` `panel`而对`sketch`程序所作的必要的改变。在这一节,我们的讨论将着重于`sizer`的实现。 **例6.8** ``` def __init__(self, parent): self.title = "Sketch Frame" wx.Frame.__init__(self, parent, -1, self.title, size=(800,600)) self.filename = "" self.sketch = SketchWindow(self, -1) self.sketch.Bind(wx.EVT_MOTION, self.OnSketchMotion) self.initStatusBar() self.createMenuBar() self.createToolBar() self.createPanel() def createPanel(self): controlPanel = ControlPanel(self, -1, self.sketch) box = wx.BoxSizer(wx.HORIZONTAL) box.Add(controlPanel, 0, wx.EXPAND) box.Add(self.sketch, 1, wx.EXPAND) self.SetSizer(box) ``` 在例6.8中,`createPanel()`方法创建了`ControlPanel`(在下面的列表中说明)的实例,并且与`box` `sizer`放在一起。`wx.BoxSizer`的构造器的唯一参数是方向,取值可以是`wx.HORIZONTAL`或`wx.VERTICAL`。接下来,这个新的`controlPanel`和先前创建的`SketchWindow`被使用`Add()`方法添加给了`sizer`。第一个参数是要被添加给`sizer`的对象。第二个参数是被`wx.BoxSizer`用作因数去决定当`sizer`的大小改变时,`sizer`应该如何调整它的孩子的尺寸。我们这里使用的是水平方向调整的`sizer`,`stretch`因数决定每个孩子的水平尺寸如何改变(坚直方向的改变由`box` `sizer`基于第三个参数来决定)。 如果第二个参数(`stretch`因数)是0,对象将不改变尺寸,无论`sizer`如何变化。如果第二个参数大于0,则`sizer`中的孩子根据因数分割`sizer`的总尺寸(类似于`wx.StatusBar`管理文本域的宽度的做法)。如果`sizer`中的所有孩子有相同的因数,那么它们按相同的比例分享放置了固定尺寸的元素后剩下的空间。这里的0表示假如用户伸展框架时,`controlPanel`不改变水平的尺寸,而1表示绘画窗口(`sketch` `window`)的尺寸要随框架的改变而改变。 `Add()`的第三个参数是另一个位掩码标志。完整的说明将在以后的章节中给出。`wx.EXPAND`指示`sizer`调整孩子的大小以完全填满有效的空间。其它的可能的选项允许孩子被按比例的调整尺寸或根据`sizer`的特定部分对齐。图6.7将帮助阐明参数及其控制的调整尺寸的方向。 这些设置的结果是当你运行这个带有`box` `sizer`的框架的时候,任何在水平方向的改变都将导致`sketch` `window`的尺寸在该方向?系母谋洌瑊 `panel`不会在该方向上改变。在坚直方向的尺寸改变导致这两个子窗口都要在坚直方向缩放。 例6.8中涉及的类`ControlPanel`结合使用了`grid`和`box` `sizer`。例6.9包含了这个类的代码。 ![](https://box.kancloud.cn/2016-08-21_57b9960aa350e.gif) **例6.9** **`ControlPanel`类** ``` class ControlPanel(wx.Panel): BMP_SIZE = 16 BMP_BORDER = 3 NUM_COLS = 4 SPACING = 4 colorList = ('Black', 'Yellow', 'Red', 'Green', 'Blue', 'Purple', 'Brown', 'Aquamarine', 'Forest Green', 'Light Blue', 'Goldenrod', 'Cyan', 'Orange', 'Navy', 'Dark Grey', 'Light Grey') maxThickness = 16 def __init__(self, parent, ID, sketch): wx.Panel.__init__(self, parent, ID, style=wx.RAISED_BORDER) self.sketch = sketch buttonSize = (self.BMP_SIZE + 2 * self.BMP_BORDER, self.BMP_SIZE + 2 * self.BMP_BORDER) colorGrid = self.createColorGrid(parent, buttonSize) thicknessGrid = self.createThicknessGrid(buttonSize) self.layout(colorGrid, thicknessGrid) def createColorGrid(self, parent, buttonSize):#1 创建颜色网格 self.colorMap = {} self.colorButtons = {} colorGrid = wx.GridSizer(cols=self.NUM_COLS, hgap=2, vgap=2) for eachColor in self.colorList: bmp = parent.MakeBitmap(eachColor) b = buttons.GenBitmapToggleButton(self, -1, bmp, size=buttonSize) b.SetBezelWidth(1) b.SetUseFocusIndicator(False) self.Bind(wx.EVT_BUTTON, self.OnSetColour, b) colorGrid.Add(b, 0) self.colorMap[b.GetId()] = eachColor self.colorButtons[eachColor] = b self.colorButtons[self.colorList[0]].SetToggle(True) return colorGrid def createThicknessGrid(self, buttonSize):#2 创建线条粗细网格 self.thicknessIdMap = {} self.thicknessButtons = {} thicknessGrid = wx.GridSizer(cols=self.NUM_COLS, hgap=2, vgap=2) for x in range(1, self.maxThickness + 1): b = buttons.GenToggleButton(self, -1, str(x), size=buttonSize) b.SetBezelWidth(1) b.SetUseFocusIndicator(False) self.Bind(wx.EVT_BUTTON, self.OnSetThickness, b) thicknessGrid.Add(b, 0) self.thicknessIdMap[b.GetId()] = x self.thicknessButtons[x] = b self.thicknessButtons[1].SetToggle(True) return thicknessGrid def layout(self, colorGrid, thicknessGrid):#3 合并网格 box = wx.BoxSizer(wx.VERTICAL) box.Add(colorGrid, 0, wx.ALL, self.SPACING) box.Add(thicknessGrid, 0, wx.ALL, self.SPACING) self.SetSizer(box) box.Fit(self) def OnSetColour(self, event): color = self.colorMap[event.GetId()] if color != self.sketch.color: self.colorButtons[self.sketch.color].SetToggle(False) self.sketch.SetColor(color) def OnSetThickness(self, event): thickness = self.thicknessIdMap[event.GetId()] if thickness != self.sketch.thickness: self.thicknessButtons[self.sketch.thickness].SetToggle(False) self.sketch.SetThickness(thickness) ``` **#1**:`createColorGrid()`方法建造包含颜色按钮的`grid` `sizer`。首先,我们创建`sizer`本身,指定列为4列。由于列数已被设定,所以按钮将被从左到右的布局,然后向下。接下来我们要求颜色的列表,并为每种颜色创建一个按钮。在`for`循环中,我们为每种颜色创建了一个方形的位图,并使用`wxPython`库中所定义的一般的按钮窗口部件类创建了带有位图的切换按钮。然后我们把按钮与事件相绑定,并把它添加到`grid`。之后,我们把它添加到字典以便在以后的代码中,易于关联颜色、`ID`和按钮。我们不必指定按钮在网格中的位置;`sizer`将为我们做这件事。 **#2**:`createThicknessGrid()`方法基本上类似于`createColorGrid()`方法。实际上,一个有进取心的程序员可以把它们做成一个通用函数。`grid` `sizer`被创建,十六个按钮被一次性添加,`sizer`确保了它们在屏幕上很好地排列。 **#3**:我们使用一个坚直的`box` `sizer`来放置网格(`grid)`。每个`grid`的第二个参数都是0,这表明`grid` `sizer`当`control` `panel`在垂直方向伸展时不改变尺寸。(由于我们已经知道`control` `panel`不在水平方向改变尺寸,所以我们不必指定水平方向的行为。)`Add()`的第四个参数是项目的边框宽度,这里使用`self.SPACING`变量指定。第三个参数`wx.ALL`是一套标志中的一个,它控制那些边套用第四个参数指定的边框宽度,`wx.ALL`表明对象的四个边都套用。最后,我们调用`box` `sizer`的`Fit()`方法,使用的参数是`control` `panel`。这个方法告诉`control` `panel`调整自身尺寸以匹配`sizer`认为所需要的最小化尺寸。通常这个方法在使用了`sizer`的窗口的构造中被调用,以确保窗口的大小足以包含`sizer`。 基类`wx.Sizer`包含了几个通用于所有`sizer`的方法。表6.9列出了最常用的方法。 **表6.9** **`wx.Sizer`的方法** ``` Add(window, proportion=0, flag=0, border=0, userData=None) Add(sizer, proportion=0, flag=0, border=0, userData=None) ``` `Add(size`, `proportion`=0,`flag`=0, `border`=0,`userData`=`None)`:第一个添加一个`wxWindow`,第二个添加一个嵌套的`sizer`,第三个添加空的空间,用作分隔符。参数`proportion`管理窗口总尺寸,它是相对于别的窗口的改变而言的,它只对`wx.BoxSizer`有意义。参数`flag`是一个位图,针对对齐、边框位置,增长有许多不同的标志,完整的列表见第十一章。参数`border`是窗口或`sizer`周围以像素为单位的空间总量。`userData`使你能够将对象与数据关联,例如,在一个子类中,可能需要更多的用于尺寸的信息。 `Fit(window)` `FitInside(window` ):调整`window`尺寸以匹配`sizer`认为所需要的最小化尺寸。这个参数的值通常是使用`sizer`的窗口。`FitInside()`是一个类似的方法,只不过将改变窗口在屏幕上的显示替换为只改变它的内部实现。它用于`scroll` `panel`中的窗口以触发滚动栏的显示。 `GetSize()`:以`wx.Size`对象的形式返回`sizer`的尺寸。 `GetPosition()`:以`wx.Point`对象的形式返回`sizer`的位置。 `GetMinSize()`:以`wx.Size`对象的形式返回完全填充`sizer`所需的最小尺寸。 `Layout()`:强迫`sizer`去重新计算它的孩子的尺寸和位置。在动态地添加或删除了一个孩子之后调用。 `Prepend(...)`:与`Add()`相同(只是为了布局的目的,把新的对象放在`sizer`列表的开头)。 `Remove(window)` `Remove(sizer)` `Remove(nth)`:从`sizer`中删除一个对象。 `SetDimension(x`, y, `width`,`height)`:强迫`sizer`按照给定的参数重新定位它的所有孩子。 有关`sizer`和嵌套`sizer`的更详细的信息请参考第11章。 ### 如何建造一个关于(about)框? `about`框是显示对话框的一个好的例子,它能够显示比纯信息框更复杂的信息。这里,你可以使用`wx.html.HtmlWindow`作为一个简单的机制来显示样式文本。实际上,`wx.html.HtmlWindow`远比我们这里演示的强大,它包括了管理用户交互以及绘制的方法。第16章涵盖了`wx.html.HtmlWindow`的特性。例6.10展示了一个类,它使用`HTML` `renderer`创建一个`about`框。 **例6.10** **使用`wx.html.HtmlWindow`作为一个`about`框** ``` class SketchAbout(wx.Dialog): text = ''' <html> <body bgcolor="#ACAA60"> <center><table bgcolor="#455481" width="100%" cellspacing="0" cellpadding="0" border="1"> <tr> <td align="center"><h1>Sketch!</h1></td> </tr> </table> </center> <p><b>Sketch</b> is a demonstration program for <b>wxPython In Action</b> Chapter 6\. It is based on the SuperDoodle demo included with wxPython, available at http://www.wxpython.org/ </p> <p><b>SuperDoodle</b> and <b>wxPython</b> are brought to you by <b>Robin Dunn</b> and <b>Total Control Software</b>, Copyright &copy; 1997-2006.</p> </body> </html>s ''' def __init__(self, parent): wx.Dialog.__init__(self, parent, -1, 'About Sketch', size=(440, 400) ) html = wx.html.HtmlWindow(self) html.SetPage(self.text) button = wx.Button(self, wx.ID_OK, "Okay") sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(html, 1, wx.EXPAND|wx.ALL, 5) sizer.Add(button, 0, wx.ALIGN_CENTER|wx.ALL, 5) self.SetSizer(sizer) self.Layout() ``` 上面的`HTML`字符串中,有一些布局和字体标记。这里的对话框合并了`wx.html.HtmlWindow`和一个`wx.ID_OK` `ID`按钮。敲击按钮则自动关闭窗口,如同其它对话框一样。一个垂直的`box` `sizer`用于管理这个布局。 图6.8显示了该对话框。 图6.8 ![](https://box.kancloud.cn/2016-08-21_57b9960ab8fc6.gif) 把它作为一个菜单项(`About)`的处理器的方法如下: ``` def OnAbout(self, event): dlg = SketchAbout(self) dlg.ShowModal() dlg.Destroy() ``` ### 如何建造一个启动画面? 显示一个好的启动画面,将给你的用户一种专业化的感觉。在你的应用程序完成一个费时的设置的时候,它也可以转移用户的注意力。在`wxPython`中,使用类`wx.SplashScreen`建造一个启动画面是很容易的。启动画面可以保持显示指定的时间,并且无论时间是否被设置,当用户在其上敲击时,它总是会关闭。 `wx.SplashScreen`类的构造函数如下: ``` wx.SplashScreen(bitmap, splashStyle, milliseconds, parent, id, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.SIMPLE_BORDER|wx.FRAME_NO_TASKBAR|wx.STAY_ON_TOP) ``` 表6.10说明了`wx.SplashScreen`构造函数的参数 **表6.10** **`wx.SplashScreen`构造函数的参数** `bitmap`:一个`wx.Bitmap`,它被显示在屏幕上。 `splashStyle`:另一个位图样式,可以是下列的结合:`wx.SPLASH_CENTRE_ON_PARENT`,`wx.SPLASH_CENTRE_ON_SCREEN`, `wx.SPLASH_NO_CENTRE`, `wx.SPLASH_TIMEOUT`, `wx.SPLASH_NO_TIMEOUT` `milliseconds`:如果`splashStyle`指定为`wx.SPLASH_TIMEOUT`,`milliseconds`是保持显示的毫秒数。 `parent`:父窗口,通常为`None`。 `id`:窗口`ID`,通常使用-1比较好。 `pos`:如果`splashStyle`取值`wx.SPLASH_NO_CENTER`的话,`pos`指定画面在屏幕上的位置。 `size`:尺寸。通常你不需要指定这个参数,因为通常使用位图的尺寸。 `style`:常规的`wxPython`框架的样式,一般使用默认值就可以了。 例6.11显示了启动画面的代码。这里我们用一具自定义的`wx.App`子类来替代了`wx.PySimpleApp`。 **例6.11** **一个启动画面的代码** ``` class SketchApp(wx.App): def OnInit(self): bmp = wx.Image("splash.png").ConvertToBitmap() wx.SplashScreen(bmp, wx.SPLASH_CENTRE_ON_SCREEN | wx.SPLASH_TIMEOUT, 1000, None, -1) wx.Yield() frame = SketchFrame(None) frame.Show(True) self.SetTopWindow(frame) return True ``` 通常,启动画面被声明在应用程序启动期间的`OnInit`方法中。启动画面将一直显示直到它被敲击或超时。这里,启动画面显示在屏幕的中央,一秒后超时。`Yield()`的调用很重要,因为它使得在应用程序继续启动前,任何未被处理的事件仍可以被继续处理。这里,`Yield()`的调用确保了在应用程序继续启动前,启动画面能够接受并处理它的初始化绘制事件。 ## 本章小结 1. 大多数的应用程序使用了诸如菜单、工具栏和启动画面这样的通常的元素。它们的使用不但对你程序的可用性有帮助,并且使你的应用程序看起来更专业。在这一章里,我们使用了一个简单的`sketch`应用程序,并且使用了工具栏、状态栏、菜单栏,通用对话框、复杂的布局、`about`框和启动画面来逐步对它作了改进。 2. 你可以使用一个设备上下文来直接对`wxPython`的显示进行绘制。不同的显示要求不同的设备上下文,然而它们共享一个通用`API`。为了平滑的显示,设备上下文可以被缓存。 3. 一个状态栏能够被自动地创建在框架的底部。它可以包含一个或多个文本域,各文本域可被独立调整尺寸和设置。 4. 菜单可以包含嵌套的子菜单,菜单项可以有切换状态。工具栏产生与菜单栏同种的事件,并且被设计来更易于对工具按钮分组布局。 5. 可以使用标准`wx.FileDialog`来管理打开和保存数据。可以使用`wx.ColourDialog`来选择颜色。 6. 使用`sizer`可以进行窗口部件的复杂布局,而不用明确地指定具体位置。`sizer`根据规则自动放置它的孩子对象。`sizer`包括的`wx.GridSizer`按二维网格来布局对象。`wx.BoxSizer`在一条线上布局项目。`sizer`可以嵌套,并且当`sizer`伸展时可以,它可以控制其孩子的行为。 7. `about`框或别的简单的对话框可以使用`wx.html.HtmlWindow`来创建。启动画面用`wx.SplashScreen`来创建。 在第一部分(`part` 1)中,我们已经涵盖了`wxPython`的基本概念,并且我们已经涉及了一些最常见的任务。在接下来的第二部分(`part` 2)中,我们将使用目前常见的问答的格式,但是我们将涉及有关`wxPython`工具包的组成和功能方面的更详细的问题。