Tutorial
Windows Drag und Drop Tutorial
von stephanbrunker | Seite 4 von 12 |
Das IDropTarget Interface
Nach dem Vorgeplänkel wird es ernst: wir erzeugen das IDropTarget Interface als Kind von IUnknown:
#Undef IDropTarget
#Undef RegisterDragDrop
Type IDropTarget EXTENDS IUnknown 'Custom IDropTarget Interface
Declare Constructor(ByVal hWin As HWND, fmtetc () As FORMATETC)
Declare Destructor()
'Methods:
'IUnknown Interface:
Declare Virtual Function QueryInterface (ByVal iid As REFIID, ByVal ppvObject As Any Ptr Ptr) As HRESULT
Declare Virtual Function AddRef () As ULong
Declare Virtual Function Release () As ULong
'IDropTarget Interface:
Declare Virtual Function DragEnter (ByVal pDataObject As IDataObject Ptr, ByVal grfKeyState As DWORD,ByVal pt As POINTL,ByVal pdwEffect As DWORD Ptr) As HRESULT
Declare Virtual Function DragOver (ByVal grfKeyState As DWORD,ByVal pt As POINTL, ByVal pdwEffect As DWORD Ptr) As HRESULT
Declare Virtual Function DragLeave () As HRESULT
Declare Virtual Function Drop (ByVal pDataObject As IDataObject Ptr,ByVal grfKeyState As DWORD,ByVal pt As POINTL,ByVal pdwEffect As DWORD Ptr) As HRESULT
'helper Function:
Declare Function RegisterSelf (ByVal pDataObject As IDataObject Ptr) As Integer
'member variables:
As HWND m_hwnd
As Integer m_fAllowDrop
As FORMATETC m_fmtetc
As Integer m_nNumFormats
As FORMATETC Ptr m_pfmtetc
As IDataObject Ptr m_dataself
End Type
Declare Function DataDrop (ByVal hWin As HWND, ByVal pfmtetc As FORMATETC Ptr, ByVal pstgmed As STGMEDIUM Ptr) As Integer
Wie auf der vorherigen Seite erwähnt, wird eine eigene Version der IUnknown-Methoden erzeugt, die sich aber nur marginal unterscheiden. Außerdem gibt es einen eigenen Constructor und Destructor, denn wir haben auch ein paar neue Member in unserem TYPE:
Constructor IDropTarget( ByVal hWin As HWND, fmtetc () As FORMATETC)
'Initialize Members:
m_fAllowDrop = FALSE
m_hwnd = hWin
'Get Formats we accept:
Dim index As ULong = UBound(fmtetc)+1
m_nNumFormats = index
m_pfmtetc = New FORMATETC[index]
Dim i As Integer
For i = 0 To index - 1
m_pfmtetc[i] = fmtetc(i)
Next i
Print "IDropTarget::Constructor [";m_nNumFormats; " Formats]"
End Constructor
Destructor IDropTarget()
Print "IDropTarget::Destructor"
If m_pfmtetc Then Delete [] m_pfmtetc
End Destructor
'IUnknown::QueryInterface
Function IDropTarget.QueryInterface (ByVal iid As REFIID, ByVal ppvObject As Any Ptr Ptr) As HRESULT
Print "IDropTarget::QueryInterface"
'if it's the right format return a pointer to the interface
If IsEqualIID ( iid, @IID_IUnknown) Or IsEqualIID( iid, @IID_IDropTarget) Then
AddRef()
*ppvObject = @this
Return S_OK
Else
*ppvObject = NULL
Return E_NOINTERFACE
End If
End Function
'IUnknown::AddRef
Function IDropTarget.AddRef () As ULong
'register a new access
m_lRefCount += 1
Print "IDropTarget::AddRef - Count: "; m_lRefCount
Return m_lRefCount
End Function
'IUnknown::Release
Function IDropTarget.Release () As ULong
'unregister an access and destroy if not needed anymore
m_lRefCount -= 1
Print "IDropTarget::Release - Count: "; m_lRefCount
If(m_lRefCount = 0) Then
Delete @this
Return 0
Else
Return m_lRefCount
End If
End Function
An der Stelle muss ich etwas ausholen über den Mechanismus des Datatransfers über OLE, was für Drag&Drop genauso gilt wie für die Zwischenablage. Jedes Format, das wir akzeptieren wollen, kommt in Form einer FORMATETC Struktur. Weil die ja programmspezifisch ist, füllen wir die in unserem Programm und übergeben sie dem Constructor als Array, denn wir können ja jede Menge verschiedener Formate akzeptieren. Das DROPFILES-Format HDROP sieht in dieser Struktur so aus:
Dim fmtetc (0 To 0) As FORMATETC
With fmtetc(0)
.cfFormat = CF_HDROP
.tymed = TYMED_HGLOBAL
.dwAspect = DVASPECT_CONTENT
.ptd = NULL
.lindex = -1
End With
Wichtig ist das Format - CF_HDROP - und wie es gespeichert wird, nämlich als GlobalMemory, also TYMED_HGLOBAL. Die weiteren Member sind erstmal uninteressant. Wenn wir Text akzepieren wollten, wäre das Format CF_TEXT und so weiter - siehe dazu in der MSDN: Transferring Shell Objects with Drag-and-Drop and the Clipboard. Im Constructor speichern wir jetzt das Array der Formate - mit NEW als Pointer erzeugt -, ein Flag ob der Drop erlaubt ist und ein Handle unseres Dropziels, denn wenn die Drop-Funktion aufgerufen wird, wollen wir ja wissen, wo etwas gedropt ist. Das Array muss dann im Destructor auch wieder freigegeben werden.
Das Interface selbst hat die Methoden DragEnter, DragOver, DragLeave und Drop, die im Verlauf einer Drag&Drop-Aktion von Windows aufgerufen werden:
- DragEnter, wenn der Cursor in ein registriertes Dropziel eintritt
- DragOver wird wiederholt gesendet, während sich der Cursor innerhalb dieses Ziels befindet
- DragLeave wenn der Cursor das Feld wieder verlässt und schließlich
- Drop wenn die Maustaste losgelassen wird und die Dropaktion durchgeführt wird.
Genau wie im IUnknown Interface habe ich Print - Mitteilungen in den Code eingebaut, die uns über die Konsole über die Aufrufe der Methoden informieren (bis auf DragOver, da wird man zugespammt).
'IDropTarget::DragEnter
Function IDropTarget.DragEnter ( ByVal pDataObject As IDataObject Ptr, ByVal grfKeyState As DWORD, ByVal pt As POINTL, ByVal pdwEffect As DWORD Ptr) As HRESULT
Print "IDropTarget::DragEnter"
'set the Cursor effect if the drag contains a format we accept, save in flag
Dim index As Integer
m_fAllowDrop = FALSE
'is the data object our own?
If pDataObject = m_dataself Then
*pdwEffect = DROPEFFECT_NONE
Return S_OK
Else
'does the dataobject contain data we want?
For index = 0 To m_nNumFormats -1
If pDataObject->QueryGetData(@m_pfmtetc[index]) = S_OK Then
m_fAllowDrop = TRUE
*pdwEffect = DROPEFFECT_COPY
Return S_OK
End If
Next index
End If
*pdwEffect = DROPEFFECT_NONE
Return S_OK
End Function
'IDropTarget::DragOver
Function IDropTarget.DragOver ( ByVal grfKeyState As DWORD, ByVal pt As POINTL, ByVal pdwEffect As DWORD Ptr) As HRESULT
'Print "IDropTarget::DragOver"
'Set cursor effect for flag
If m_fAllowDrop Then
*pdwEffect = DROPEFFECT_COPY
Else
*pdwEffect = DROPEFFECT_NONE
End If
Return S_OK
End Function
'IDropTarget::DragLeave
Function IDropTarget.DragLeave () As HRESULT
Print "IDropTarget::DragLeave"
'reset the AllowDrop member:
m_fAllowDrop = FALSE
Return S_OK
End Function
'IDropTarget::Drop
Function IDropTarget.Drop ( ByVal pDataObject As IDataObject Ptr, ByVal grfKeyState As DWORD, ByVal pt As POINTL, ByVal pdwEffect As DWORD Ptr) As HRESULT
Print "IDropTarget::Drop"
Dim stgmed As STGMEDIUM
Dim index As Integer
'if we actually allow the drop, get the STGMEDIUM
If m_fAllowDrop Then
For index = 0 To m_nNumFormats -1
If pDataObject->GetData(@m_pfmtetc[index], @stgmed) = S_OK Then
'Call the drop extraction function
DataDrop(m_hwnd, @m_pfmtetc[index], @stgmed)
*pdwEffect = DROPEFFECT_COPY
'release the data using the COM API
ReleaseStgMedium(@stgmed)
m_fAllowDrop = FALSE
Return S_OK
End If
Next index
End If
'If we don't want a drop, do nothing and set the appropriate cursor:
*pdwEffect = DROPEFFECT_NONE
m_fAllowDrop = FALSE
Return S_OK
End Function
DragEnter, DragOver und DragLeave machen im Prinzip erstmal das selbe, nämlich eine Rückmeldung geben ob wir einen Drop erlauben - was in erster Linie vom Inhalt abhängt, außerdem wird das in einem Flag gespeichert. Die Abfrage nach pDataself ist für den Fall enthalten, dass wir selbst einen Drag starten und wir nicht wollen, dass wir auf uns selbst droppen. Ich hatte das zuerst als shared - Variable, aber die muss man vorher definieren. Um das zu vermeiden, habe ich eine helper-Function definiert, mit der man den Pointer zum eigenen Dataobject im Type ablegen kann:
'helper Function:
Function IDropTarget.RegisterSelf (ByVal pDataObject As IDataObject Ptr) As Integer
m_dataself=pDataObject
Return TRUE
End Function
Da wir die Standardimplementaion der Interfaces neu definieren, ändern sich damit auch die damit verbundenen Funktionen. Damit es keine Compilerfehler gibt, wurde ganz am Anfang die RegisterDragDrop ebenfalls entdefiniert und muss jetzt mit dem neuen Type wieder neu deklariert werden:
Declare Function RegisterDragDrop Alias "RegisterDragDrop" (ByVal As HWND, ByVal As IDropTarget Ptr ) As HRESULT
Die Daten liegen also in Form eines IDataObject Interfaces vor (das wir im Detail später behandeln werden). Fürs erste brauchen wir nur den richtigen TYPE dafür (denn der vorhandene ist ja alte Version). Wenn wir nur einen Drop akzeptieren wollen, reicht eine Abstracte Variante um die VTABLE und die Struktur zu erstellen, mit der wir auf das von Windows bereitgestellte Objekt zugreifen können:
#Undef IDataObject
Type IDataObject EXTENDS IUnknown 'custom IDataObject Interface, requires Buxfix for IsEqualIID!
'IDataObject Interface:
Declare Abstract Function GetData ( ByVal pfmtetc As FORMATETC Ptr, ByVal pstgmed As STGMEDIUM Ptr) As HRESULT
Declare Abstract Function GetDataHere ( ByVal pfmtetc As FORMATETC Ptr, ByVal pstgmed As STGMEDIUM Ptr) As HRESULT
Declare Abstract Function QueryGetData ( ByVal pfmtetc As FORMATETC Ptr) As HRESULT
Declare Abstract Function GetCanonicalFormatEtc ( ByVal pfmtetc As FORMATETC Ptr, ByVal pfmtetc2 As FORMATETC Ptr) As HRESULT
Declare Abstract Function SetData ( ByVal pfmtetc As FORMATETC Ptr, ByVal pstgmed As STGMEDIUM Ptr, ByVal As BOOL) As HRESULT
Declare Abstract Function EnumFormatEtc ( ByVal dwDirection As DWORD, ByVal ppEnumFmtetc As IEnumFORMATETC Ptr Ptr) As HRESULT
Declare Abstract Function DAdvise ( ByVal pfmtetc As FORMATETC Ptr, ByVal advf As DWORD, ByVal pAdvSink As IAdviseSink Ptr, ByVal pDwConnection As PDWORD) As HRESULT
Declare Abstract Function DUnadvise ( ByVal dwConnection As DWORD) As HRESULT
Declare Abstract Function EnumDAdvise ( ByVal ppEnumAdvise As IEnumSTATDATA Ptr Ptr) As HRESULT
End Type
Wir rufen daraus die Funktionen QueryGetData und GetData auf, die uns einfach nur sagen, ob die Dropquelle ein bestimmtes Format anbietet und bei letzterer Funktion auch den zweiten Teil des Formats anbietet, nämlich das STGMEDIUM. In unserem Fall ist das einfach der tatsächliche Pointer im HGlobal, wo unsere Daten liegen. Das übergeben wir einer Funktion DataDrop, die dann in unserem Hauptprogramm angesiedelt ist und die Daten extrahiert. Weil bei diesen Datentransfergeschichten meistens der Empfänger für den Verbleib des reservierten Speichers zuständig ist, müssen wir das STGMEDIUM auch noch freigeben. Wie wir sehen, ist der Code für das Interface ziemlich universell, denn wir können beliebig viele Formate akzeptieren und extrahieren und die tatsächliche Extraktion findet außerhalb des Interfaces statt. Was ich hier jetzt nicht implementiert habe ist die Rückmeldung auf verschiedene Tastenkombinationen - die in GrfKeyState übermittelt werden. Zum Beispiel könnte man bei gedrückter STRG-Taste statt dem Kopieren-Cursor einen Verschieben-Cursor erzeugen, es gibt verschiedene DROPEFFECTs dafür. Ebenfalls steckt da die Maustaste (links oder rechts) drin, der Explorer öffnet ja zum Beispiel ein Kontextmenü nach dem Drop, wenn mit der rechten Maustaste gezogen wurde. Wenn gewünscht, müsste man diese Variablen ebenfalls an die DataDrop-Funktion übergeben.
Zusätzliche Informationen und Funktionen | |||||||
---|---|---|---|---|---|---|---|
|
|