🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 第十八章 使用wxPython的其他功能 本章内容: * 放置对象到剪贴板上 * 拖放 * 传送和获取自定义对象 * 使用`wx.Timer`设置定时的事件 * 编写多线程的`wxPython`应用程序 ## 放置对象到剪贴板上 在`wxPython`中,剪贴板和拖放特性是紧密相关的。期间,内部窗口的通信是由使用`wx.DataObject`类或它的子类的一个实例作为中介的。`wx.DataObject`是一个特殊的数据对象,它包含描述输出数据格式的元数据。我们将从剪贴板入手,然后我们将讨论拖放的不同处理。 对于一个剪切和粘贴操作,有三个元素: * `source(`源) * `clipboard(`剪贴板) * `target(`目标) 如果`source`是在你的应用程序中,那么你的应用程序负责创建`wx.DataObject`的一个实例并把它放到剪贴板对象。通常`source`都是在你的应用程序的外部。 这里的`clipboard`是一个全局对象,它容纳数据并在必要时与操作系统的剪贴板交互。 `target`对象负责从剪贴板获取`wx.DataObject`并把它转换为对你的应用程序有用的那一类数据。 ### 得到剪贴板中的数据 如果你想你的应用程序能够引起一个剪贴事件,也就是说你想能够将数据剪切或复制到剪贴板,把数据放置到一个`wx.DataObject`里面。`wx.DataObject`知道自己能够被读写何种格式的数据。这点是比较重要的,例如如果你当时正在写一个词处理程序并希望给用户在粘贴时选择无格式文本的粘贴或丰富文本格式的粘贴的情况。然而大多数时候,在你的剪贴板行为中不需要太强大或太灵活的性能。对于最常用的情况,`wxPython`提供了三个预定义的`wx.DataObject`的子类:纯文本,位图图像和文件名。 要传递纯文本,可以创建类`wx.TextDataObject`的一个实例,使用它如下的构造器: ``` wx.TextDataObject(text="") ``` 参数`text`是你想传递到剪贴的文本。你可以使用`Text(text)`方法来设置该文本,你也可以使用`GetText()`方法来得到该文本,你还可以使用`GetTextLength()`方法来得到该文本的长度。 一旦你创建了这种数据对象后,接着你必须访问剪贴板。系统的剪贴板在`wxPython`中是一个全局性的对象,名为`wx.TheClipboard`。要使用它,可以使用它的`Open()`方法来打开它。如果该剪贴板被打开了则该方法返回`True`,否则返回`False`。如果该剪贴板正在被另一应用程序写入的话,该剪贴板的打开有可能会失败,因此在使用该剪贴板之前,你应该检查打开方法的返回值。当你使用完剪贴板之后,你应该调用它的 `Close()`方法来关闭它。打开剪贴板会阻塞其它的剪贴板用户的使用,因此剪贴板打开的时间应该尽可能的短。 ### 处理剪贴板中的数据 一旦你有了打开的剪贴板,你就可以处理它所包含的数据对象。你可以使用`SetData(data)`来将你的对象放置到剪贴板上,其中参数`data`是一个`wx.DataObject`实例。你可以使用方法`Clear()`方法来清空剪贴板。如果你希望在你的应用程序结束后,剪贴板上的数据还存在,那么你必须调用方法`Flush()`,该方法命令系统维持你的数据。否则,该`wxPython`剪贴板对象在你的应用程序退出时会被清除。 下面是一段添加文本到剪贴板的代码: ``` text_data = wx.TextDataObject("hi there") if wx.TheClipboard.Open(): wx.TheClipboard.SetData(text_data) wx.TheClipboard.Close() ``` ### 获得剪贴板中的文本数据 从剪贴板中获得文本数据也是很简单的。一旦你打开了剪贴板,你就可以调用`GetData(data)`方法,其中参数`data`是`wx.DataObject`的一些特定的子类的一个实例。如果剪贴板中的数据能够以与方法中的数据对象参数相一致的某种格式被输出的话,该方法的返回值则为`True`。这里,由于我们传递进的是一个`wx.TextDataObject`,那么返回值`True`就意味该剪贴板能够被转换到纯文本。下面是一段样例代码: ``` text_data = wx.TextDataObject() if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(text_data) wx.TheClipboard.Close() if success: return text_data.GetText() ``` 注意,当你从剪贴板获取数据时,数据并不关心是哪个应用程序将它放置到剪贴板的。剪贴板中的数据本身被底层的操作系统所管理,`wxPython`的责任是确保格式的匹配及你能够得到你能够处理的数据格式。 ### 实战剪贴板 在这一节,我们将显示一个简单的例子,它演示了如何与剪贴板交换数据。它是一个有着两个按钮的框架,它使用户能够复制和粘贴文本。当你运行这个例子时,结果将会如图18.1所示。 **图18.1** ![](https://box.kancloud.cn/2016-08-21_57b996487c412.gif) 例18.1是产生图18.1的代码。 **例18.1** **剪贴板交互示例** ``` #-*- encoding:UTF-8 -*- import wx t1_text = """\ The whole contents of this control will be placed in the system's clipboard when you click the copy button below. """ t2_text = """\ If the clipboard contains a text data object then it will be placed in this control when you click the paste button below. Try copying to and pasting from other applications too! """ class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="Clipboard", size=(500,300)) p = wx.Panel(self) # create the controls self.t1 = wx.TextCtrl(p, -1, t1_text, style=wx.TE_MULTILINE|wx.HSCROLL) self.t2 = wx.TextCtrl(p, -1, t2_text, style=wx.TE_MULTILINE|wx.HSCROLL) copy = wx.Button(p, -1, "Copy") paste = wx.Button(p, -1, "Paste") # setup the layout with sizers fgs = wx.FlexGridSizer(2, 2, 5, 5) fgs.AddGrowableRow(0) fgs.AddGrowableCol(0) fgs.AddGrowableCol(1) fgs.Add(self.t1, 0, wx.EXPAND) fgs.Add(self.t2, 0, wx.EXPAND) fgs.Add(copy, 0, wx.EXPAND) fgs.Add(paste, 0, wx.EXPAND) border = wx.BoxSizer() border.Add(fgs, 1, wx.EXPAND|wx.ALL, 5) p.SetSizer(border) # Bind events self.Bind(wx.EVT_BUTTON, self.OnDoCopy, copy) self.Bind(wx.EVT_BUTTON, self.OnDoPaste, paste) def OnDoCopy(self, evt):#Copy按钮的事件处理函数 data = wx.TextDataObject() data.SetText(self.t1.GetValue()) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(data)#将数据放置到剪贴板上 wx.TheClipboard.Close() else: wx.MessageBox("Unable to open the clipboard", "Error") def OnDoPaste(self, evt):#Paste按钮的事件处理函数 success = False data = wx.TextDataObject() if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(data)#从剪贴板得到数据 wx.TheClipboard.Close() if success: self.t2.SetValue(data.GetText())#更新文本控件 else: wx.MessageBox( "There is no data in the clipboard in the required format", "Error") app = wx.PySimpleApp() frm = MyFrame() frm.Show() app.MainLoop() ``` ### 获得剪贴板中的文本数据 从剪贴板中获得文本数据也是很简单的。一旦你打开了剪贴板,你就可以调用`GetData(data)`方法,其中参数`data`是`wx.DataObject`的一些特定的子类的一个实例。如果剪贴板中的数据能够以与方法中的数据对象参数相一致的某种格式被输出的话,该方法的返回值则为`True`。这里,由于我们传递进的是一个`wx.TextDataObject`,那么返回值`True`就意味该剪贴板能够被转换到纯文本。下面是一段样例代码: ``` text_data = wx.TextDataObject() if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(text_data) wx.TheClipboard.Close() if success: return text_data.GetText() ``` 注意,当你从剪贴板获取数据时,数据并不关心是哪个应用程序将它放置到剪贴板的。剪贴板中的数据本身被底层的操作系统所管理,`wxPython`的责任是确保格式的匹配及你能够得到你能够处理的数据格式。 ### 实战剪贴板 在这一节,我们将显示一个简单的例子,它演示了如何与剪贴板交换数据。它是一个有着两个按钮的框架,它使用户能够复制和粘贴文本。当你运行这个例子时,结果将会如图18.1所示。 **图18.1** ![](https://box.kancloud.cn/2016-08-21_57b996487c412.gif) 例18.1是产生图18.1的代码。 **例18.1** **剪贴板交互示例** ``` #-*- encoding:UTF-8 -*- import wx t1_text = """\ The whole contents of this control will be placed in the system's clipboard when you click the copy button below. """ t2_text = """\ If the clipboard contains a text data object then it will be placed in this control when you click the paste button below. Try copying to and pasting from other applications too! """ class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="Clipboard", size=(500,300)) p = wx.Panel(self) # create the controls self.t1 = wx.TextCtrl(p, -1, t1_text, style=wx.TE_MULTILINE|wx.HSCROLL) self.t2 = wx.TextCtrl(p, -1, t2_text, style=wx.TE_MULTILINE|wx.HSCROLL) copy = wx.Button(p, -1, "Copy") paste = wx.Button(p, -1, "Paste") # setup the layout with sizers fgs = wx.FlexGridSizer(2, 2, 5, 5) fgs.AddGrowableRow(0) fgs.AddGrowableCol(0) fgs.AddGrowableCol(1) fgs.Add(self.t1, 0, wx.EXPAND) fgs.Add(self.t2, 0, wx.EXPAND) fgs.Add(copy, 0, wx.EXPAND) fgs.Add(paste, 0, wx.EXPAND) border = wx.BoxSizer() border.Add(fgs, 1, wx.EXPAND|wx.ALL, 5) p.SetSizer(border) # Bind events self.Bind(wx.EVT_BUTTON, self.OnDoCopy, copy) self.Bind(wx.EVT_BUTTON, self.OnDoPaste, paste) def OnDoCopy(self, evt):#Copy按钮的事件处理函数 data = wx.TextDataObject() data.SetText(self.t1.GetValue()) if wx.TheClipboard.Open(): wx.TheClipboard.SetData(data)#将数据放置到剪贴板上 wx.TheClipboard.Close() else: wx.MessageBox("Unable to open the clipboard", "Error") def OnDoPaste(self, evt):#Paste按钮的事件处理函数 success = False data = wx.TextDataObject() if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(data)#从剪贴板得到数据 wx.TheClipboard.Close() if success: self.t2.SetValue(data.GetText())#更新文本控件 else: wx.MessageBox( "There is no data in the clipboard in the required format", "Error") app = wx.PySimpleApp() frm = MyFrame() frm.Show() app.MainLoop() ``` 在下一节中,我们将讨论如何传递其它格式的数据,如位图。 ### 传递其它格式的数据 经由剪贴板交互位图几乎与传递文本相同。你所使用的相关的数据对象子类是`wx.BitmapDataObject`,其`get`*方法和`set`*方法分别是`GetBitmap()`和`SetBitmap(bitmap)`。经由该数据对象与剪贴板交互的数据对象必须是`wx.Bitmap`类型的。 最后一个预定义的数据对象类型是`wx.FileDataObject`。通常该数据对象被用于拖放中(将在18.2节中讨论),例如当你将一个文件从你的资源管理器或查找窗口放置到你的应用程序上时。你可以使用该数据对象从剪贴板接受文件名数据,并且你可以使用方法`GetFilenames()`来从该数据对象获取文件名,该方法返回一个文件名的列表,列表中的每个文件名是已经被添加到剪贴板的文件名。你可以使用该数据对象的`AddFile(file)`方法来将数据放置到剪贴板上,该方法将一个文件名字符串添加到该数据对象。这里没有其它的方法用于直接处理列表,所以这就要靠你自己了。本章的稍后部份,我们将讨论如何经由剪贴板传送自定义对象,以及如何拖放对象。 ## 拖放源 拖放是一个类似剪切和粘贴的功能。它是在你的应用程序的不同部分之间或两个不同的应用程序之间传送数据。由于管理数据和格式几乎是相同的,所以`wxPython`同样使用`wx.DataObject`族来确保对格式作恰当的处理。 拖放和剪切粘贴的最大不同是,剪切粘贴信赖于中介剪贴板的存在。因为是剪贴板管理数据,所以源程序将数据传送后就不管之后的事情了。这对于拖放却不然,源应用程序不仅虽要创建一个拖动管理器来服务于剪贴板,而且它也必须等待目标应用程序的响应。不同于一个剪贴板的操作,在拖放中,是目标应用来决定操作是一个剪贴或拷贝,所以源应用必须等待以确定传送的数据所用的目的。 通常,对源的拖动操作是在一个事件处理函数中进行,通常是一个鼠标事件,因为拖动通常都随鼠标的按下事件发生。创建一个拖动源要求四步: 1、创建数据对象 2、创建`wx.DropSource`实例 3、执行拖动操作 4、取消或允许释放 **步骤1** **创建一个数据对象** 这第一步是创建你的数据对象。这在早先的剪贴板操作中有很好的说明。对于简单的数据,使用预定义的`wx.DataObject`的子类是最简单的。有了数据对象后,你可以创建一个释放源实例 **步骤2** **创建释放源实例** 接下来的步骤是创建一个`wx.DropSource`实例,它扮演类似于剪贴板这样的传送角色。`wx.DropSource`的构造函数如下: ``` wx.DropSource(win, iconCopy=wx.NullIconOrCursor, iconMove=wx.NullIconOrCursor, iconNone=wx.NullIconOrCursor) ``` 参数`win`是初始化拖放操作的窗口对象。其余的三个参数用于使用自定义的图片来代表鼠标的拖动意义(拷贝、移动、取消释放)。如果这三个参数没有指定,那么使用系统的默认值。在微软的`Windows`系统上,图片必须是`wx.Cursor`对象,对于`Unix`则应是`wx.Icon`对象——`Mac OS`目前忽略你的自定义图片。 一旦你有了你的`wx.DropSource`实例,那么就可以使用方法`SetData(data)`来将你的数据对象关联到`wx.DropSource`实例。接下来我们将讨论实际的拖动。 **步骤3** **执行拖动** 拖动操作通过调用释放源的方法`DoDragDrop(flags=wx.Drag_CopyOnly)`来开始。参数`flags`表示目标可对数据执行的何种操作。取值有`wx.Drag_AllowMove`,它表示批准执行一个移动或拷贝,`wx.Drag_DefaultMove`表示不仅允许执行一个移动或拷贝,而且做默认的移动操作,`wx.Drag_CopyOnly`表示只执行一个拷贝操作。 **步骤4** **处理释放** `DoDragDrop()`方法直到释放被目标取消或接受才会返回。在此期间,虽然绘制事件会继续被发送,但你的应用程序的线程被阻塞。`DoDragDrop()`的返回值基于目标所要求的操作,取值如下: `wx.DragCancel`(对于取消操作而言) `wx.DragCopy `(对于拷贝操作而言) `wx.DragMove `(对于移动操作而言) `wx.DragNone `(对于错误而言) 对这些返回值的响应由你的应用程序来负责。通常对于响应移动要删除被拖动的数据外,对于拷贝则是什么也不用做。 ### 实战拖动 例18.2显示了一个完整的拖动源控件,适合于通过拖动上面的箭头图片到你的系统的任何接受文本的应用上(如`Microsoft word`)。图18.2图示了这个例子。 **图18.2** ![](https://box.kancloud.cn/2016-08-21_57b996489623f.gif) **例18.2** **一个小的拖动源控件** ``` #-*- encoding:UTF-8 -*- import wx class DragController(wx.Control): """ Just a little control to handle dragging the text from a text control. We use a separate control so as to not interfere with the native drag-select functionality of the native text control. """ def __init__(self, parent, source, size=(25,25)): wx.Control.__init__(self, parent, -1, size=size, style=wx.SIMPLE_BORDER) self.source = source self.SetMinSize(size) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) def OnPaint(self, evt): # draw a simple arrow dc = wx.BufferedPaintDC(self) dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.Clear() w, h = dc.GetSize() y = h/2 dc.SetPen(wx.Pen("dark blue", 2)) dc.DrawLine(w/8, y, w-w/8, y) dc.DrawLine(w-w/8, y, w/2, h/4) dc.DrawLine(w-w/8, y, w/2, 3*h/4) def OnLeftDown(self, evt): text = self.source.GetValue() data = wx.TextDataObject(text) dropSource = wx.DropSource(self)#创建释放源 dropSource.SetData(data)#设置数据 result = dropSource.DoDragDrop(wx.Drag_AllowMove)#执行释放 # if the user wants to move the data then we should delete it # from the source if result == wx.DragMove: self.source.SetValue("")#如果需要的话,删除源中的数据 class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="Drop Source") p = wx.Panel(self) # create the controls label1 = wx.StaticText(p, -1, "Put some text in this control:") label2 = wx.StaticText(p, -1, "Then drag from the neighboring bitmap and\n" "drop in an application that accepts dropped\n" "text, such as MS Word.") text = wx.TextCtrl(p, -1, "Some text") dragctl = DragController(p, text) # setup the layout with sizers sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(label1, 0, wx.ALL, 5) hrow = wx.BoxSizer(wx.HORIZONTAL) hrow.Add(text, 1, wx.RIGHT, 5) hrow.Add(dragctl, 0) sizer.Add(hrow, 0, wx.EXPAND|wx.ALL, 5) sizer.Add(label2, 0, wx.ALL, 5) p.SetSizer(sizer) sizer.Fit(self) app = wx.PySimpleApp() frm = MyFrame() frm.Show() app.MainLoop() ``` 接下来,我们将给你展示目标处的拖放。 ## 拖放到的目标 实现拖放到的目标的步骤基本上借鉴了实现拖放源的步骤。其中最大的区别是,实现拖放源,你可以直接使用类`wx.DropSource`,而对于目标,你首先必须写你的自定义的`wx.DropTarget`的子类。一旦你有了你的目标类,你将需要创建它的一个实例,并通过使用`wx.Window`的`SetDropTarget(target)`方法将该实例与任一 `wx.Window`的实例关联起来。设置了目标后,`wx.Window`的实例(不论它是一个窗口,一个按钮,一个文本域或其它的控件)就变成了一个有效的释放目标。为了在你的释放目标上接受数据,你也必须创建一个所需要类型的`wx.DataObject`对象,并使用释放目标方法`SetDataObject(data)`将`wx.DataObject`对象与释放目标关联起来。在实际释放操作前,你需要预先定义数据对象,以便该释放目标能够正确地处理格式。要从目标获取该数据对象,有一个方法`GetDataObject()`。下面的样板代码使得释放目标能够接受文本(仅能接受文本)。这是因为数据对象已经被设置为`wx.TextDataObject`的一个实例。 ``` class MyDropTarget(wx.DropTarget): def __init__(self): self.data = wx.TextDataObject() self.SetDataObject(data) target = MyDataTarget() win.SetDropTarget(target) ``` ### 使用你的释放到的目标 当一个释放发生时,你的`wx.DropTarget`子类的各种事件函数将被调用。其中最重要的是`OnData(x, y, default)`,它是你必须在你自定义的释放目标类中覆盖的一个事件方法。参数`x,y`是释放时鼠标的位置。`default`参数是`DoDragDrop()`的四个取值之一,具体的值基于操作系统,传递给`DoDragDrop()`标志和当释放发生时修饰键的状态。在且仅在`OnData()`方法中,你可以调用`GetData()`。`GetData()`方法要求来自释放源的实际的数据并把它放入与你的释放目标对象相关联的数据对象中。`GetData()`不返回数据对象,所以你通常应该用一个实例变量来包含你的数据对象。下面是关于`MyDropTarget.OnData()`的样板代码: ``` def OnData(self, x, y, default): self.GetData() actual_data = self.data.GetText() # Do something with the data here... return default ``` `OnData()`的返回值应该是要导致操作——你应该返回参数`default`的值,除非这儿有一个错误并且你需要返回`wx.DragNone`。一旦你有了数据,你就可以对它作你想做的。记住,由于`OnData()`返回的是关于所导致操作的相关信息,而非数据本身,所以如果你想在别处使用该数据的话,你需要将数据放置在一个实例变量里面(该变量在该方法外仍然可以被访问)。 在释放操作完成或取消后,返回自`OnData()`的导致操作类型的数据被从`DoDragDrop()`的返回,并且释放源的线程将继续进行。 在`wx.DropTarget`类中有五个`On...`方法,你可以在你的子类中覆盖它们以在目标被调用时提供自定义的行为。我们已经见过了其中的`OnData()`,另外的如下: `OnDrop(x, y)` `OnEnter(x, y, default) ` `OnDragOver(x, y, default)` `OnLeave()` 其中的参数`x, y, default`同`OnData()`。你不必覆盖这些方法,但是如果你想在你的应用程序中提供自定义的功能的话,你可以覆盖这些方法。 当鼠标进入释放到的目标区域时,`OnEnter()`方法首先被调用。你可以使用该方法来更新一个状态窗口。该方法返回如果释放发生时要执行的操作(通常是`default`的值)或`wx.DragNone`(如果你不接受释放的话)。该方法的返回值被`wxPython`用来指定当鼠标移动到窗口上时,哪个图标或光标被用作显示。当鼠标位于窗口中时,方法`OnDragOver()`接着被调用,它返回所期望的操作或`wx.DragNone`。当鼠标被释放并且释放(`drop)`发生时,`OnDrop()`方法被调用,并且它默认调用`OnData()`。最后,当光标退出窗口时`OnLeave()`被调用。 与数据对象一同,`wxPython`提供了两个预定义的释放到的目标类来涵盖最常见的情况。除了在这些情况中预定义的类会为你处理`wx.DataObject`,你仍然需要创建一个子类并覆盖一个方法来处理相关的数据。关于文本,类`wx.TextDropTarget`提供了可覆盖的方法`OnDropText(x, y, data)`,你将使用通过覆盖该方法来替代覆盖`OnData()`。参数`x,y`是释放到的坐标,参数`data`是被释放的字符串,该字符串你可以立即使用面不用必须对数据对象作更多的查询。如果你接受新的文本的话,你的覆盖应该返回`True`,否则应返回`False`。对于文件的释放,相关的预定义的类是`wx.FileDropTarget`,并且可覆盖的方法是`OnDropFiles(x, y, filenames)`,参数`filenames`是被释放的文件的名字的一个列表。另外,必要的时候你可以处理它们,当完成时可以返回`True`或`False`。 ### 实战释放 例18.3中的代码显示了如何创建一个框架(窗口)用以接受文件的释放。你可以通过从资源管理器或查找窗口拖动一个文件到该框架(窗口)上来测试例子代码,并观查显示在窗口中的关于文件的信息。图18.3是运行后的结果。 **图18.3** ![](https://box.kancloud.cn/2016-08-21_57b99648a9b5d.gif) **例18.3** **文件释放到的目标的相关代码** ``` #-*- encoding:UTF-8 -*- import wx class MyFileDropTarget(wx.FileDropTarget):#声明释放到的目标 def __init__(self, window): wx.FileDropTarget.__init__(self) self.window = window def OnDropFiles(self, x, y, filenames):#释放文件处理函数数据 self.window.AppendText("\n%d file(s) dropped at (%d,%d):\n" % (len(filenames), x, y)) for file in filenames: self.window.AppendText("\t%s\n" % file) class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="Drop Target", size=(500,300)) p = wx.Panel(self) # create the controls label = wx.StaticText(p, -1, "Drop some files here:") text = wx.TextCtrl(p, -1, "", style=wx.TE_MULTILINE|wx.HSCROLL) # setup the layout with sizers sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(label, 0, wx.ALL, 5) sizer.Add(text, 1, wx.EXPAND|wx.ALL, 5) p.SetSizer(sizer) # make the text control be a drop target dt = MyFileDropTarget(text)#将文本控件作为释放到的目标 text.SetDropTarget(dt) app = wx.PySimpleApp() frm = MyFrame() frm.Show() app.MainLoop() ``` 到目前为止,我们还是局限于对`wxPython`的预定义的对象的数据传送的讨论。接下来,我们将讨论如何将你自己的数据放到剪贴板上。 ## 传送自定义对象 使用`wxPython`的预定义的数据对象,你只能工作于纯文本、位图或文件。而更有创建性的是,你应该让你自定义的对象能够在应用之间被传送。在这一节,我将给你展示如何给你的`wxPython`应用程序增加更高级的性能,如传送自定义的数据对象和以多种格式传送一个对象。 ### 传送自定义的数据对象 尽管文本、位图的数据对象和文件名的列表对于不同的使用已经足够了,但有时你仍然需要传送自定义的对象,如你自己的图形格式或一个自定义的数据结构。接下来,在保留对你的对象将接受的数据的类型的控制时,我们将涉及传送自定义数据对象的机制。该方法的局限是它只能工作在`wxPython`内,你不能使用这个方法来让其它的应用程序去读你的自定义的格式。要将`RTF`(丰富文本格式)传送给`Microsoft Word`,该机制将不工作。 要实现自定义的数据传送,我们将使用`wxPython`的类`wx.CustomDataObject`,它被设计来用于处理任意的数据。`wx.CustomDataObject`的构造器如下: ``` wx.CustomDataObject(format=wx.FormatInvalid) ``` 参数`format`技术上应该是类`wx.DataFormat`的一个实例,但为了我们的目的,我们可以只给它传递一个字符串,数据类型的责任由`wxPython`来考虑。我们只需要这个字符串作为自定义格式的一个标签,以与其它的区分开来。一旦我们有了我们自定义的数据实例,我们就可以使用方法`SetData(data)`将数据放入到自定义的数据实例中。参数`data`必须是一个字符串。下面是一段样板代码: ``` data_object = wx.CustomDataObject("MyNiftyFormat") data = cPickle.dumps(my_object) data_object.SetData(data) ``` 在这段代码片断之后,你可以将`data_object`传递到剪贴板或另一个数据源,以继续数据的传送。 ### 得到自定义对象 要得到该对象,需要执行相同的基本步骤。对于从剪贴板获取,先创建相同格式的一个自定义数据对象,然后得到数据并对得到的数据进行逆`pickle`操作(`pickle`有加工的意思)。 ``` data_object = wx.CustomDataObject("MyNiftyFormat") if wx.TheClipboard.Open(): success = wx.TheClipboard.GetData(data_object) wx.TheClipboard.Close() if success: pickled_data = data_object.GetData() object = cPickle.loads(pickled_data) ``` 拖放工作是类似的。使用已`pickle`的数据设置释放源的数据对象,并将设置的数据对象给你的自定义的数据对象,数据的目标在它的`OnData()`方法中对数据进行逆`pickle`操作并把数据放到有用的地方。 创建自定义对象的另一个方法是建造你自己的`wx.DataObject`子类。如果你选择这条途径,那么你会希望实现你自己的诸如`wx.PyDataObjectSimple`(用于通常的对象),或`wx.PyTextDataObject`,`wx.PyBitmapDataObject, `或`wx.PyFileDataObject`的一个子类。这将使你能够覆盖所有必要的方法。 ### 以多种格式传送对象 使用`wxPython`的数据对象来用于数据传送的最大好处是,数据对象了解数据格式。一个数据对象甚至能够用多种的格式来管理相同的数据。例如,你可能希望你自己的应用程序能够接受你的自定义的文本格式对象的数据,但是你仍然希望其它的应用能够以纯文本的格式接受该数据。 管理该功能的机制是类`wx.DataObjectComposite`。目前,我们所见过的所有被继承的数据对象都是`wx.DataObjectSimple`的子类。`wx.DataObjectComposite`的目的是将任意数量的简单数据对象合并为一个数据对象。该合并后的对象能够将它的数据提供给与构成它的任一简单类型匹配的一个数据对象。 要建造一个合成的数据对象,首先要使用一个无参的构造器`wx.DataObjectComposite()`作为开始,然后使用`Add(data, preferred=False)`分别增加简单数据对象。要建造一个合并了你的自定义格式和纯文本的数据对象,可以如下这样: ``` data_object = wx.CustomDataObject("MyNiftyFormat") data_object.SetData(cPickle.dumps(my_object)) text_object = wx.TextDataObject(str(my_object)) composite = wx.DataObjectComposite() composite.Add(data_object) composite.Add(text_object) ``` 此后,将这个合成的对象传递给剪贴板或你的释放源。如果目标类要求一个使用了自定义格式的对象,那么它接受已`pickle`的对象。如果它要求纯文本的数据,那么它得到字符串表达式。 下节内容:我们将给你展示如何使用一个定时器来管理定时事件。 ## 使用wx.Timer来设置定时事件 有时你需要让你的应用程序产生基于时间段的事件。要得到这个功能,你可以使用类`wx.Timer`。 ### 产生EVT_TIMER事件 对`wx.Timer`最灵活和最有效的用法是使它产生`EVT_TIMER`,并将该事件如同其它事件一样进行绑定。 **创建定时器** 要创建一个定时器,首先要使用下面的构造器来创建一个`wx.Timer`的实例。 ``` wx.Timer(owner=None, id=-1) ``` 其中参数`owner`是实现`wx.EvtHandler`的实例,即任一能够接受事件通知的`wxPython`控件或其它的东西。参数`id`用于区分不同的定时器。如果没有指定`id`,则`wxPython`会为你生成一个`id`号。如果当你创建定时器时,你不想设置参数`owner`和`id`,那么你可以以后随时使用`SetOwner(owner=None, id=`-1)方法来设置,它设置同样的两个参数。 **绑定定时器** 在你创建了定时器之后,你可以如下面一行的代码来在你的事件处理控件中绑定`wx.EVT_TIMER`事件。 ``` self.Bind(wx.EVT_TIMER, self.OnTimerEvent) ``` 如果你需要绑定多个定时器到多个处理函数,你可以给`Bind`函数传递每个定时器的`ID`,或将定时器对象作为源参数来传递。 ``` timer1 = wx.Timer(self) timer2 = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnTimer1Event, timer1) self.Bind(wx.EVT_TIMER, self.OnTimer2Event, timer2) ``` **启动和停止定时器** 在定时器事件被绑定后,你所需要做的所有事情就是启动该定时器,使用方法`Start(milliseconds=`-1, `oneShot=False)`。其中参数`milliseconds`是毫秒数。这将在经过`milliseconds`时间后,产生一个`wx.EVT_TIMER`事件。如果`milliseconds=`-1,那么将使用早先的毫秒数。如果`oneShot`为`True`,那么定时器只产生`wx.EVT_TIMER`事件一次,然后定时器停止。否则,你必须显式地使用`Stop()`方法来停止定时器。 例18.4使用了定时器机制来驱动一个数字时钟,并每秒刷新一次显示。 **例18.4** **一个简单的数字时钟** ``` #-*- encoding:UTF-8 -*- import wx import time class ClockWindow(wx.Window): def __init__(self, parent): wx.Window.__init__(self, parent) self.Bind(wx.EVT_PAINT, self.OnPaint) self.timer = wx.Timer(self)#创建定时器 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)#绑定一个定时器事件 self.timer.Start(1000)#设定时间间隔 def Draw(self, dc):#绘制当前时间 t = time.localtime(time.time()) st = time.strftime("%I:%M:%S", t) w, h = self.GetClientSize() dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.Clear() dc.SetFont(wx.Font(30, wx.SWISS, wx.NORMAL, wx.NORMAL)) tw, th = dc.GetTextExtent(st) dc.DrawText(st, (w-tw)/2, (h)/2 - th/2) def OnTimer(self, evt):#显示时间事件处理函数 dc = wx.BufferedDC(wx.ClientDC(self)) self.Draw(dc) def OnPaint(self, evt): dc = wx.BufferedPaintDC(self) self.Draw(dc) class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="wx.Timer") ClockWindow(self) app = wx.PySimpleApp() frm = MyFrame() frm.Show() app.MainLoop() ``` **确定当前定时器的状态** 你可以使用方法`IsRunning()`来确定定时器的当前状态,使用方法`GetInterval()`来得到当前的时间间隔。如果定时器正在运行并且只运行一次的话,方法`IsOneShot()`返回`True`。 `wx.TimerEvent`几乎与它的父类`wx.Event`是一样的,除了它不包括`wx.GetInterval()`方法来返回定时器的时间间隔外。万一你将来自多个定时器的事件绑定给了相同的处理函数,并希望根据特定的定时器的事件来做不同的动作的话,可使用事件方法`GetId()`来返回定时器的`ID`,以区别对待。 ### 学习定时器的其它用法 另一种使用定时器的方法是子类化`wx.Timer`。在你的子类中你可以覆盖方法`Notify()`。在父类中,该方法每次在定时器经过指定的时间间隔后被自动调用,它触发定时器事件。然而你的子类没有义务去触发一个定时器事件,你可以在该`Notify()`方法中做你想做的事,以响应定时器的时间间隔。 要在未来某时触发一个特定的行为,有一个被称为`wx.FutureCall`的类可以使用。它的构造器如下: ``` wx.FutureCall(interval, callable, *args, **kwargs) ``` 一旦它被创建后,`wx.FutureCall`的实例将等待`interval`毫秒,然后调用传递给参数`callable`的对象,参数*`args, `**`kwargs`是`callable`中的对象所要使用的。`wx.FutureCall`只触发一次定时事件。 下节内容提示:创建一个多线程的`wxPython`应用程序 ## 创建一个多线程的wxPython应用程序 在大多数的`GUI`应用程序中,在应用程序的后台中长期执行一个处理过程而不干涉用户与应用程序的其它部分的交互是有好处的。允许后台处理的机制通常是产生一个线程并在该线程中长期执行一个处理过程。对于`wxPython`的多线程,在这一节我们有两点需要特别说明。 最重要的一点是,`GUI`的操作必须发生在主线程或应用程序的主循环所处的地方中。在一个单独的线程中执行`GUI`操作对于无法预知的程序崩溃和调试来说是一个好的办法。基于技术方面的原因,如许多`Unix`的`GUI`库不是线程安全性的,以及在微软`Windows`下`UI`对象的创建问题,`wxPython`没有设计它自己的发生在多线程中的事件,所以我们建议你也不要尝试。 上面的禁令包括与屏幕交互的任何项目,尤其包括`wx.Bitmap`对象。 对于`wxPython`应用程序,关于所有`UI`的更新,后台线程只负责发送消息给`UI`线程,而不关心`GUI`的更新。幸运的是,`wxPython`没有强制限定你能够有的后台线程的数量。 在这一节,我们将关注几个`wxPython`中实现多线程的方法。最常用的技术是使用`wx.CallAfter()`函数,一会我们会讨论它。然后,我们将看一看如何使用`Python`的队列对象来设置一个并行事件队列。最后,我们将讨论如何为多线程开发一个定制的解决方案。 ### 使用全局函数wx.CallAfter() 例18.5显示了一个使用线程的例子,它使用了`wxPython`的全局函数`wx.CallAfter()`,该函数是传递消息给你的主线程的最容易的方法。`wx.CallAfter()`使得主线程在当前的事件处理完成后,可以对一个不同的线程调用一个函数。传递给`wx.CallAfter()`的函数对象总是在主线程中被执行。 图18.4显示了多线程窗口的运行结果。 **图18.4** ![](https://box.kancloud.cn/2016-08-21_57b99648bfb66.gif) 例18.5显示了产生图18.4的代码 **例18.5** **使用`wx.CallAfter()`来传递消息给主线程的一个线程例子** ``` #-*- encoding:UTF-8 -*- import wx import threading import random class WorkerThread(threading.Thread): """ This just simulates some long-running task that periodically sends a message to the GUI thread. """ def __init__(self, threadNum, window): threading.Thread.__init__(self) self.threadNum = threadNum self.window = window self.timeToQuit = threading.Event() self.timeToQuit.clear() self.messageCount = random.randint(10,20) self.messageDelay = 0.1 + 2.0 * random.random() def stop(self): self.timeToQuit.set() def run(self):#运行一个线程 msg = "Thread %d iterating %d times with a delay of %1.4f\n" \ % (self.threadNum, self.messageCount, self.messageDelay) wx.CallAfter(self.window.LogMessage, msg) for i in range(1, self.messageCount+1): self.timeToQuit.wait(self.messageDelay) if self.timeToQuit.isSet(): break msg = "Message %d from thread %d\n" % (i, self.threadNum) wx.CallAfter(self.window.LogMessage, msg) else: wx.CallAfter(self.window.ThreadFinished, self) class MyFrame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, title="Multi-threaded GUI") self.threads = [] self.count = 0 panel = wx.Panel(self) startBtn = wx.Button(panel, -1, "Start a thread") stopBtn = wx.Button(panel, -1, "Stop all threads") self.tc = wx.StaticText(panel, -1, "Worker Threads: 00") self.log = wx.TextCtrl(panel, -1, "", style=wx.TE_RICH|wx.TE_MULTILINE) inner = wx.BoxSizer(wx.HORIZONTAL) inner.Add(startBtn, 0, wx.RIGHT, 15) inner.Add(stopBtn, 0, wx.RIGHT, 15) inner.Add(self.tc, 0, wx.ALIGN_CENTER_VERTICAL) main = wx.BoxSizer(wx.VERTICAL) main.Add(inner, 0, wx.ALL, 5) main.Add(self.log, 1, wx.EXPAND|wx.ALL, 5) panel.SetSizer(main) self.Bind(wx.EVT_BUTTON, self.OnStartButton, startBtn) self.Bind(wx.EVT_BUTTON, self.OnStopButton, stopBtn) self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) self.UpdateCount() def OnStartButton(self, evt): self.count += 1 thread = WorkerThread(self.count, self)#创建一个线程 self.threads.append(thread) self.UpdateCount() thread.start()#启动线程 def OnStopButton(self, evt): self.StopThreads() self.UpdateCount() def OnCloseWindow(self, evt): self.StopThreads() self.Destroy() def StopThreads(self):#从池中删除线程 while self.threads: thread = self.threads[0] thread.stop() self.threads.remove(thread) def UpdateCount(self): self.tc.SetLabel("Worker Threads: %d" % len(self.threads)) def LogMessage(self, msg):#注册一个消息 self.log.AppendText(msg) def ThreadFinished(self, thread):#删除线程 self.threads.remove(thread) self.UpdateCount() app = wx.PySimpleApp() frm = MyFrame() frm.Show() app.MainLoop() ``` 上面这个例子使用了`Python`的`threading`模块。上面的代码使用`wx.CallAfter(func,`*`args)`传递方法给主线程。这将发送一个事件给主线程,之后,事件以标准的方式被处理,并触发对`func(`*`args)`的调用。因些,在这种情况中,线程在它的生命周期期间调用`LogMessage()`,并在线程结束前调用`ThreadFinished()`。 ### 使用队列对象管理线程的通信 尽管使用`CallAfter()`是管理线程通信的最简单的方法,但是它并不是唯一的机制。你可以使用`Python`的线程安全的队列对象去发送命令对象给`UI`线程。这个`UI`线程应该在`wx.EVT_IDLE`事件的处理函数中写成需要接受来自该队列的命令。 本质上,你要为线程通信设置一个并行的事件队列。如果使用这一方法,那么工作线程在当它们增加一个命令对象到队列时,应该调用全局函数`wx.WakeUpIdle()`以确保尽可能存在在一个空闲事件。这个技术比`wx.CallAfter()`更复杂,但也更灵活。特别是,这个机制可以帮助你在后台线程间通信,虽然所有的`GUI`处理仍在主线程上。 ### 开发你自已的解决方案 你也可以让你自己的工作线程创建一个`wxPython`事件(标准的或自定义的),并使用全局函数`wx.PostEvent(window, event)`将它发送给`UI`线程中的一个特定的窗口。该事件被添加到特定窗口的未决事件队列中,并且`wx.WakeUpIdle`自动被调用。这条道的好处是事件将遍历的`wxPython`事件设置,这意味你将自由地得到许多事件处理能力,坏处是你不得不自已管理所有的线程和`wx.CallAfter()`函数所为你做的事件处理。 ## 本章小结 1、拖放和剪贴板事件是非常相似的,两者都使用了`wx.DataObject`来作为数据格式的媒介。除了可以创建自定义的格式以外,还存在着默认的数据对象,包括文本,文件和位图。在剪贴板的使用中,全局对象`wx.TheClipboard`管理数据的传送并代表底层系统的剪贴板。 2、对于拖放操作,拖动源和拖动到的目标一起工作来管理数据传送。拖动源事件被阻塞直到拖动到的目标作出该拖动操作是否有效的判断。 3、类`wx.Timer`使你能够设置定时的事件。 4、线程在`wxPython`是可以实现的,但时确保所有的`GUI`活动发生在主线程中是非常重要的。你可以使用函数`wx.CallAfter()`来管理内部线程的通信问题。