Tutorial
Windows Drag und Drop Tutorial
von stephanbrunker | Seite 8 von 12 |
Listview mit Drag und Drop
Der erste Schritt beim Zusammenbauen unseres Codes ist, die ganzen Interfaces umzudefinieren und zu laden in unserer Include-Sektion:
#Include "windows-fix.bi"
#Undef DoDragDrop
#Include "IUnknown.bas"
#Include "IEnumFormatEtc.bas"
#Include "IDataObject.bas"
#Include "IDropTarget.bas"
#Include "IDropSource.bas"
Declare Function DoDragDrop Alias "DoDragDrop" (ByVal As IDataObject Ptr, ByVal As IDropSource Ptr, ByVal As DWORD, ByVal As PDWORD) As HRESULT
'Include our own *.bi
#Include "Dialog.bi"
Aus der DragDrop2 - Variante bleibt der DropTarget bzw. DataDrop-Teil unverändert, hierfür wird lediglich das vorbereitete Element aktiviert, dass einen Drop auf sich selbst erkennt. Ansonsten kommt neu eine DataTransfer-Funktion hinzu, die ich am besten in Einzelteilen erkläre:
Function DataTransfer(ByVal hLSV1 As HWND, ByVal cFlags As UInteger, ByVal cAction As UInteger) As Integer
Dim As UInteger nFiles,index,j
Dim numFormats As UInteger
'get filenames and paths
nFiles=ListView_GetSelectedCount(hLSV1)
If nFiles = 0 Then Return FALSE
Dim filetitle(0 To nFiles -1) As String
Dim filepath (0 To nFiles -1) As String
Dim getzstring As ZString * MAX_PATH
Dim As Integer selected = -1
Print "Drag Files:"
For index=0 To nFiles-1
selected = ListView_GetNextItem(hLSV1,selected,LVNI_SELECTED)
ListView_GetItemText(hLSV1,selected,1,@getzstring,SizeOf(getzstring))
filepath(index) = getzstring
ListView_GetItemText(hLSV1,selected,0,@getzstring,SizeOf(getzstring))
filetitle(index) = getzstring
Print " ",filepath(index) & filetitle(index)
Next index
der erste Teil holt die selektierten Zeilen aus dem ListView-Element und speichert sie in zwei Arrays - das hat erstmal nix mit Drag und Drop zu tun. Wenn wir uns an den Drop und das CF_HDROP-Format erinnern, ist das ein 0-separierter und mit 00-terminierter String, den wir als nächstes zusammenbauen:
'hGlobal pointers
Dim pGlob1 As hglobal
'=======================
' CF_HDROP - Format
'=======================
If cFlags And C_HDROP Then
'paths of the selected files in a 0-seperated and 00-terminated String
Dim pdrop As DROPFILES Ptr
Dim filelist As String
numFormats += 1
For index=0 To nFiles-1
filelist &= filepath(index) & filetitle(index) & Chr(0)
Next index
filelist &= Chr(0)
'copy to GLOBAL
pGlob1 = GlobalAlloc(GPTR_ ,SizeOf(DROPFILES)+Len(filelist))
pdrop = GlobalLock(pGlob1)
'fill DROPFILES
pdrop->pFiles = SizeOf(DROPFILES)
pdrop->fWide = FALSE
'copy filelist after DROPFILES
memcpy(pdrop+1,StrPtr(filelist),Len(filelist))
GlobalUnlock(pGlob1)
End If
Außer diesem String besteht das Format noch aus einem DROPFILES-Typ, der aber eigentlich nichts wichtiges enthält, danach folgt der String. Wir reservieren (GlobalAlloc) ausreichend GLOBAL Memory für beides zusammen und kopieren es dann mit memcpy dort hin, nachdem wir den Speicher mit GlobalLock für uns gesichert haben.
Der nächste Schritt: FORMATETC und STGMEDIUM erstellen. Das haben wir soweit auch schon gehabt, nur dass wir die zwei Strukturen jetzt selbst füllen, FormatEtc ist in diesem Fall CF_HDROP als TYMED_HGLOBAL und das STGMEDIUM enthält den Pointer zu dem GlobalMemory, wo wir die Daten abgelegt haben:
'=====================================
' generate FORMATETC and STGMEDIUM
'=====================================
Dim fmtetc (0 To numFormats -1) As FORMATETC
Dim stgmed (0 To numFormats -1) As STGMEDIUM
index = 0
If (cFlags And C_HDROP) Then
'Format HDROP
With fmtetc(index)
.cfFormat = CF_HDROP
.tymed = TYMED_HGLOBAL
.dwAspect = DVASPECT_CONTENT
.ptd = NULL
.lindex = -1
End With
With stgmed(index)
.tymed = TYMED_HGLOBAL
.hGlobal = pGlob1
.pUnkForRelease = NULL
End With
index +=1
EndIf
Das ganze hängt in IF-Schleifen weil wir wieder modular programmieren wollen und verschiedene Datentransferszenarien mit einer einzigen Funktion abdecken wollen, das sind die cFlags- und cAction-Attribute beim Funktionsaufruf. Deshalb sind fmtetc und stgmed auch Arrays, denn wir können ja beliebig viele verschiedene Formate anbieten - wobei wir aber dann keinen Einfluss mehr darauf haben, welches Format der Empfänger dann nimmt. Der Rest geht dann ganz schnell: um unsere zwei Strukturen definieren wir jetzt das IDataObject, erzeugen eine DropSource, sagen unserem immer noch aktiven DropTarget, was unser eigenes Objekt ist und übergeben an Windows mit DoDragDrop.
Dim pDataObject As IDATAOBJECT Ptr = New IDATAOBJECT(fmtetc(),stgmed())
'Drag&Drop operation
If cAction And DRAGDROP Then
Dim dweffect As DWORD
Dim pDropSource As IDROPSOURCE Ptr = New IDROPSOURCE()
pDropTarget->RegisterSelf(pDataObject) 'register our own Data at our DropTarget
DoDragDrop(pDataObject,pDropSource,DROPEFFECT_COPY,@dweffect)
Print "Dragdrop complete"
pDropSource->Release()
pDropTarget->RegisterSelf(NULL)
EndIf
pDataObject->Release()
Return -1
End Function
Nach Beendigung der Aktion geben wir die Interfaces wieder frei, die sich damit selbst löschen. Wir erkennen, dass der User versucht etwas zu draggen durch die LVN_BEGINDRAG - Message, die als WM_NOTIFY versendet wird:
Case LVN_BEGINDRAG
'Begin a drag and drop operation
DataTransfer(hLSV1,C_HDROP, DRAGDROP)
Und damit sollte das ganze (eigentlich) schon funktionieren. Der Teufel steckt wie gehabt hier extrem im Detail, da während der ganzen Aktion hauptsächlich Windows die Funktionen aufruft und das extrem schwer zu debuggen ist, da kann man leicht Tage mit der Fehlersuche verbringen.
Als kleine Hilfe kann ich nur auf das Zeug verweisen, was unsere Print-Anweisungen mitteilen, das sollte mit Beginn der Dragoperation ungefähr so aussehen:
Drag Files:
(ein oder mehrere Dateipfade)
IUnknown::Constructor
IDataObject::Constructor [ 1 Formats ]
IUnknown::Constructor
IDropSource::Constructor
IDropSource::QueryInterface
IDropSource::QueryInterface
IDataObject::QueryInterface
IDataObject::QueryInterface
IDataObject::QueryInterface
IDataObject::AddRef - Count : 2
IDataObject::AddRef - Count : 3
IDataObject::QueryInterface
IDataObject::QueryInterface
IDataObject::QueryInterface
IDataObject::Release - Count : 2
IDataObject::QueryInterface
IDataObject::AddRef - Count : 3
IDataObject::QueryInterface
IDataObject::AddRef - Count : 4
IDataObject::EnumFormatEtc
IUnknown::Constructor
IEnumFormatEtc::Constructor [ 1 Formats ]
IEnumFormatEtc::Next [copied: 1 ]
IEnumFormatEtc::Next [copied: 0 ]
IEnumFormatEtc::Reset
IEnumFormatEtc::Next [copied: 1 ]
IEnumFormatEtc::Next [copied: 0 ]
IEnumFormatEtc::Reset
IEnumFormatEtc::Release - Count: 0
IEnumFormatEtc::Destructor
IDataObject::QueryGetData-> LookupFormatEtc: MATCH @ 0
IDataObject::AddRef - Count : 5
IDropTarget::DragEnter
DATASELF
...
Die Erklärung, soweit ich sie mir zusammenreimen konnte: Die Constructoren sind erst einmal klar, die haben wir ja selbst veranlasst. Dann gibt es von DoDragDrop aus einige QueryInterface-Anfragen an IDragSource und IDataObject, die aber meistens nach anderen Interfaces fragen und deshalb keine neue Instanz erzeugen, bis dann die Funktion sich mit unserem Dataobjekt dann doch einigt und nach dem Pilgerschritt-Verfahren einige Instanzen erzeugt und wieder freigibt. Danach wird der Enumerator aufgerufen, der aus welchem Grund auch immer zweimal durchläuft. Dann kommt die erste Anfrage nach dem Inhalt unseres Datenobjekts und der Rest klingt dann schon nach unserem eigenen IDropTarget, was da anfragt. Wenn man aber so weit gekommen ist, müsste es auch funktionieren.
Dieses Beispielprogramm funktioniert jedenfalls:
DragDrop3.zip
Zusätzliche Informationen und Funktionen | |||||||
---|---|---|---|---|---|---|---|
|
|