被wxpython的拖放(DnD,Drag and Drop)操作困扰了很久,因此有了这篇blog。
wxpython的DnD机制在网上的资料很少,往往只有语焉不详的例子。
那么,在这些范例背后所隐藏的,是怎样的DnD机制呢?
根据wx的文档,DnD本质上就是一个C&P的操作——一个数据传递的操作。
这个操作牵涉到哪些对象呢?
wxWindow类或者它的派生类的对象: srcWnd
wxWindow类或者它的派生类的对象: dstWnd
wxDropSource类的派生类的对象: dropSource
wxDropTarget类的派生类的对象:dropTarget
wxDataObject类的对象:dataObject
首先,srcWnd根据鼠标事件决定是否开始拖放。在一开始,产生一个dropSource,接着,将要拖放的数据(通过DataObject)和srcWnd放入dropSrouce中,然后,执行dropSource.DoDragDrop()。这样就进入了拖放的流程。
在DoDragDrop中,系统会不断检测鼠标的移动,一旦检测到鼠标所在的窗口可以接受dropSource中的数据时(检测要满足的条件如后所述),则将调用这个窗口拥有的dropTarget对象(这个对象一般在窗口创建时就建立了)的一系列函数:鼠标进入窗口时,调用dstWnd.dropTarget.OnEnter;鼠标离开窗口时,调用dstWnd.dropTarget.OnLeave;在窗口中移动时,调用dstWnd.dropTarget.OnDragOver;在松开鼠标按键(一般是左键)时,调用dstWnd.dropTarget.OnData。这样就完成了拖放的过程。
在完成了拖放过程之后,DoDragDrop的返回值将标识这次拖放是数据拷贝还是数据移动,源窗口可以根据这个返回值来处理源数据。这个返回值默认为wxDragCopy。一般是由dropTarget的OnData函数设定。
那么,在这个过程中,系统是如何知道目的窗口是可以接受源窗口所传递的数据呢?这就牵涉到一个数据格式的问题。
在wxWidgets中,数据格式通过wxDataFormat类来体现。wxDataFormat有两个成员:type和id,前者是一个整型,后者则是一个字符串。用户要么使用type来识别数据类型,要么使用id来识别数据类型。用户根据wxDataObject中的wxDataFormat信息,来决定如何处理通过wxDataObject::GetDataHere所获得的字节数组——换言之,根据格式决定数据的序列化和反序列化操作。
如上所述,系统也是根据dropSource中的格式和dropTarget中的格式是否一致来判定一个窗口是否能成为拖放目的地的。具体来说,判定过程如下:
1.鼠标所在的窗口有DropTarget对象
2.这个对象的DataObject中的格式与拖放源中的格式相同
以上即wxWidgets的DnD原理。在wxPython中,情形又有些细微的差别。这些差别主要体现在数据传递上。
wxPython为用户提供了DropDropTarget,TextDropTarget等简单的DropTarget派生类,用于处理常见的拖放操作,比如文字或者文件的拖放。用户想要处理这些拖放时,只要简单的继承这些类,并实现自己的OnDropText、OnDropFile等函数即可。
然而,要实现自定义的数据类的拖放,又应该怎么办?这时候就要用到wx.CustomDataObject这个类。这个类派生自wx.DataObjectSimple,一次只能持有一个数据,而不是DataObject里的多个数据。因此,也只有一种数据格式。这个数据格式在wx.CustomDataObject创建时通过构造函数传入。可以传入一个整形,或者是一个字符串。一般都是用字符串,否则全局的整形管理起来是相当麻烦的一件事情。
具体而言:
#产生数据源 global ds ds=wx.DropSource(self) #产生数据对象 tdo=wx.CustomDataObject("mydata") #设置数据对象的数据 tdo.SetData("hello world") #设置数据源的数据对象 ds.SetData(tdo) #开始拖放操作 dragResult=ds.DoDragDrop() if dragResult==wx.DragCopy: pass elif dragResult==wx.DragMove: pass else: pass
以上是使用wx.DropSource和wx.CustomDataObject的一段代码。这里要注意的是,这个数据源是全局的。之所以这么做的原因是,wxPython的DropTarget.OnData的接口中没有像C++的接口那样将数据源的数据对象传入。因此,只能通过这种设置全局变量的方式来获取数据源中的数据对象。
我们可以看到,这个数据源所携带的数据的类型id——在wx.CustomObject的构造函数中传入——是"mydata"。所以我们需要一个能处理“mydata”的DropSource:
class MyDropTarget(wx.PyDropTarget): def __init__(self,dstWnd): wx.PyDropTarget.__init__(self) #通过设置数据对象,设置DropTarget的数据格式 cdo=wx.CustomDataObject("mydata") self.SetDataObject(cdo) self.target=dstWnd return def OnEnter(self,x,y,d): #print "OnEnter" if hasattr(self.target,"OnDragIn"): self.target.OnDragIn(x,y,d) return d return d def OnLeave(self): if hasattr(self.target,"OnDragOut"): self.target.OnDragOut() return def OnData(self,x,y,d): #print "on data,"+str(d) cdo=wx.CustomDataObject("mydata") #根据数据格式获取数据 print ds.GetDataObject().GetDataHere(cdo.GetFormat()) return d def OnDragOver(self,x,y,d): if hasattr(self.target,"OnDragOver"): self.target.OnDragOver(x,y,d) return d
上面这段代码中,在构造函数里设定了DropTarget的数据格式,使得系统能够知道:对“mydata”这类数据的拖放,持有这个MyDropTarget对象的窗体——dstWnd是可以接收源数据的。
在OnEnter、OnLeave、OnDragOver这几个函数中,都会检查和调用dstWnd中的函数,使得dstWnd有机会显示一些拖动的效果,比如改变背景色等。值得一提的是,dstWnd的这些函数中,在设置好拖动效果以后,都需要调用一下Refresh()函数,否则显示效果是不会改变的。
在OnData函数中,通过数据格式从全局的数据源——ds中获取数据。所获取的数据是一串string。这是wxPython的简化处理。这样可以利用python的pickle,将python对象序列化成string传递。换言之,在真正使用时,获取数据之后,还应该用pickle进行一次反序列化操作。
最后要提的是,一个窗口设定自己的DropTarget这项工作,是通过wx.Window.SetDropTarget这个函数来完成的。
以上即wxPython的DnD的大致内容,其实并不复杂。