# 第十八章 使用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**

例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**

例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**

**例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**

**例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**

例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()`來管理內部線程的通信問題。
- 活學活用wxPython
- 前言
- 致謝
- 關于本書
- 第一部分
- 第一章 歡迎使用wxPython
- 第二章 給wxPython程序一個堅實的基礎
- 第三章 在事件驅動環境中開發
- 第四章 用PyCrust使得wxPython更易處理
- 第五章 繪制藍圖
- 第六章 使用wxPython基本構件
- 第二部分 基礎wxPython
- 第七章 使用基礎控件
- 第八章 將構件放入窗體中
- 第九章 通過對話框讓用戶選擇
- 第十章 創建和使用wxPython菜單
- 第十一章 使用sizer放置構件
- 第十二章 操作基本圖像
- 第三部分 高級wxPython
- 第十三章 建造列表控件并管理列表項
- 第十四章 網格控件
- 第十五章 樹形控件
- 第十六章 在應用程序中加入HTML
- 第十七章 wxPython的打印構架
- 第十八章 使用wxPython的其他功能