Autor Beitrag
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19272
Erhaltene Danke: 1740

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Mi 04.08.04 15:31 
Hallo Leute!

Ich habe jetzt schon viele Fragen gelesen, wie man Dateien vom eigenen Fenster per Drag-and-Drop in eine andere Anwendung ziehen lassen kann. Deshalb erläutere ich das hier mal, zudem ist das ein ganz gutes Bespiel für die Verwendung von Interfaces.

Nan benötigt diese Funktion:
ausblenden Delphi-Quelltext
1:
2:
function DoDragDrop(dataObj: IDataObject; dropSource: IDropSource; 
    dwOKEffects: Longint; var dwEffect: Longint): HResult; stdcall;

Die befindet sich in der Ole32.dll.

Den Quelltext habe ich aus einer Komponente von Angus Johnson von 1997, diese kann bei mir heruntergeladen werden. (Damit kann man Dateitransfer in beide Richtungen per Drag-and-Drop machen, es gibt jeweils eine entsprechende Komponente, aber die Annahme von Dateien per Delphi-Programm wurde ja bereits von vielen erläutert...)
Quelltext der Komponente + Quelltext der Demo:
www.buchmanager-berl...mp;file=dragdrop.zip
oder über den Mirror
www.sj-berlin.de/dow...4pa9fds/dragdrop.zip
// EDIT: URLs aktualisiert. Die Downloads gehen wieder! Exe-Demo hinzugefügt.
Wer sich die Demo erstmal ansehen möchte ohne diese selbst kompilieren zu müssen, der kann sich die kompilierte ausführbare Datei herunterladen. Der Quelltext ist bei der Komponente dabei.
Ausführbare Exe-Datei der Demo:
www.buchmanager-berl...=Dragdrop%20Demo.zip
oder über den Mirror
www.sj-berlin.de/dow.../Dragdrop%20Demo.zip

Die Komponenten sollten von Delphi 2.01 bis Delphi 2006 funktionieren. (D2 von Angus selbst, D3, D7 und D2006 von mir getestet).
// EDIT: Delphi 2006 getestet.

Wer sich nicht für die Interna interessiert (was ich bei dem Thema niemandem verübeln kann, da es doch recht knifflig für Anfänger ist :-) ), der kann sich einfach nur die Komponente herunterladen. Ich finde es aber doch sehr wichtig, sich einmal mit dem Thema auseinanderzusetzen.

Erstmal kurz etwas über Interfaces:
In Interfaces werden allgemeine Methoden zur Verfügung gestellt, die von Objekten, die von diesem Interface abgeleitet werden, implementiert werden müssen. (Ggf. nur mit dem Hinweis, dass die Methode nicht implementiert ist.)
Das Programm, das das Interface benutzt, muss dann gar nicht wissen, was genau das Interface macht, es benutzt einfach die zur Verfügung gestellten Methoden.

Na ja, das war seehr kurz, aber ich hoffe dennoch einigermaßen verständlich...


Zunächst: Was muss überhaupt gemacht werden?

Wir brauchen Objekte, die von den Interface-Typen abegeleitet sind, die der Funktion DoDragDrop übergeben werden müssen.
Eine Beschreibung der Interfaces und der also zu implementierenden Methoden finden sich (zufälligerweise ;-)) in der Win32 SDK Reference.

Die möchte ich hier nicht komplett hineinkopieren, wer die also ansehen möchte, kann das auf meiner Homepage tun:
www.sj-berlin.de/htm...p_sdk/dodragdrop.htm
oder über den Mirror
www.buchmanager-berl...p_sdk/dodragdrop.htm
// EDIT: URLs aktualisiert. Die Links gehen wieder!
Die Informationen stammen aus der SDK Referenz von Microsoft und werden hier nur zu Lernzwecken zur Verfügung gestellt. Diese Informationen sind auch in Microsofts MSDN zu finden:
msdn.microsoft.com/l...9da-41a4e5a61315.asp
Da ich nicht sicher bin, ob diese Seite auch weiterhin immer unter diesem Link zu finden ist, habe ich die Informationen auch unter obigen Links auf meine Server gestellt.

Wir benötigen ein Interface vom Typ IDataObject und eins vom Typ IDropSource.

Die im SDK genannten Routinen müssen entsprechend implementiert werden.

Ich benutze die von Angus eingeführten Bezeichnungen für die von den Interfaces abgeleiteten Objekte.


Zunächst zu MyDataObject, abgeleitet von IDataObject:
ausblenden Delphi-Quelltext
1:
TMyDataObject = class(IDataObject)					


Im Konstruktor muss die Dateiliste übergeben und in eine interne Liste, die auch erzeugt werden muss, gefüttert werden.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
var
  i: integer;
begin
  inherited Create;
  FileList := TStringList.create;
  FileList.assign(sl);
  FileListBytes := 1;
  for i := 1 to FileList.count do
    inc(FileListBytes,length(FileList[i-1])+1);


Im Destruktor muss die interne Liste entfernt werden.

In QueryInterface wird überprüft, ob das angegebene Interface unterstützt wird und ggf. ein Pointer darauf gegeben. Die übergebene Interface-ID wird also mit den unterstützten Interfaces, also IUnknown und IDataObject verglichen. (IUnknown wird immer implementiert, es ist der Vorfahr jedes Interfaces.)

Die Methoden AddRef, Release erhöhen und vermindern den Referenzzähler.

Die folgenden Methoden geben nur zurück, dass sie von dem aktuellen Interface nicht implementiert werden:
GetDataHere, GetCanonicalFormatEtc, SetData, DAdvise, DUnadvise, EnumDAdvise

In EnumFormatEtc wird ein Datentyp angefordert, der Informationen über die von dem Interface benutzten Daten liefert. Mit dem Parameter DATADIR_GET wird dieser Datentyp für die Methode GetData, mit DATADIR_SET für SET_DATA für SetData angefordert.
Es sollen aber keine Daten gesetzt werden, daher gibt es auch SetData nicht, daher wird da auch zurückgegeben, dass es nicht implementiert ist.

Nun aber zu den beiden wichtigsten Methoden dieses Interfaces:
QueryGetData und GetData

Mit QueryGetData fragt ein Prozess an, ob ein Aufruf von GetData mit den Angaben erfolgreich wäre.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
  with formatetc do begin
    if cfFormat <> CF_HDROP then
      //nur CF_HDROP wird unterstützt
      Result := DV_E_FORMATETC
      //Es ist ein anderer Parameter angegeben, 
      //also "ungültiger formatetc" zurückgeben
    else if (tymed and TYMED_HGLOBAL) = 0 then
      Result := DV_E_TYMED
      //falscher tymed, also "ungültiger tymed" zurückgeben
      //mit tymed wird das speichermedium angegeben, das für die 
      //Operation benutzt wird
    else
      Result := S_OK;
      //Die Werte scheinen Ok
  end;


Mit GetData werden dann erst tatsächlich die Daten abgefragt:
(keine Angst, ist nicht so kompliziert, wie es erstmal aussieht)
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
function TMyDataObject.GetData(var formatetcIn: TFormatEtc; 
  var medium: TStgMedium): HResult; stdcall;
var h: HGlobal;
    i, offset: integer;
begin
  Result := DV_E_FORMATETC;
  //Falls was nicht geht, erstmal den Rückgabewert auf 
  //"ungültiger Wert für formatetc" setzen
  if not Failed(QueryGetData(formatetcIn)) then begin
    //Wenn schon die Anfrage nach den Daten schiefgeht, nix weiter machen
    h := GlobalAlloc(GMEM_MOVEABLE or GMEM_ZEROINIT,
      FileListBytes+sizeof(tdropfiles));
    //Jetzt wird Speicher gebraucht, in dem die Daten übergeben werden können
    if h = 0 then begin
      //Der einzige sinnvolle Grund, warum dies fehlschlägt, ist dass kein 
      //Speicher mehr da ist, also entsprechend angeben und Vorgang abbrechen
      Result:= E_OUTOFMEMORY;
      Exit;
    end;
    
    ptrdropfile:=globallock(h);
    //globallock sperrt den Speicherbereich (vor z.B. verschieben)
    //und gibt den Anfang des Bereichs zurück
    with ptrdropfile^ do begin
      pfiles:=sizeof(Tdropfiles);
      pt.x:=0;
      pt.y:=0;
      longint(fnc) := 0;
      longint(Fwide) := 0;
    end;
    //An die Stelle, an der der Speicherbereich beginnt die Daten
    //für TDropFiles hinschreiben

    //Jetzt müssen noch die einzelnen Dateinamen in den Speicherbereich 
    //geschrieben werden. Dabei wird jeder Dateiname nullterminiert 
    //abgespeichert, ebenso muss der ganze Speicherbereich nullterminiert 
    //sein. Nullterminiert bedeutet, dass am Ende ein Nullzeichen #0
    //angehängt wird. (entspricht PChar ~= String + #0)
    offset := sizeof(tdropfiles);
    //offset gibt an, wo gerade der als nächstes zu beschreibende
    //Speicher liegt
    for i := 1 to FileList.count do begin
      if i = FileList.count then
        strPcopy( pchar(longint(ptrdropfile)+offset), FileList[i-1]+#0#0)
        //An die Stelle hinter den letzten Daten schreiben, also
        //ptrdropfile, der Anfang des Speichers, + offset, die aktuelle 
        //Position
        //Wenn es das letzte Element ist, mit zwei Nullzeichen terminieren
      else
        strPcopy( pchar(longint(ptrdropfile)+offset), FileList[i-1]+#0);
      offset := offset + length(FileList[i-1])+1;
      //offset um die Länge des aktuell hinzugefügten Dateinamens +
      //die Länge des Nullzeichens (also 1) erhöhen
    end;

    globalunlock(h);
    //Speicherbereich wieder entsperren

    with medium do begin
      tymed:=TYMED_HGLOBAL;
      hGlobal := h;
      unkForRelease := nil;
    end;
    //Und das Medium inklusive Handle auf den Speicherbereich übergeben

    result:=S_OK;
    //Die Operation war erfolgreich
  end;
end;


Jetzt ist es wieder zu einfach? Ok, dann machen wir gleich weiter, es fehlt noch das zweite Interface sowie das IEnumFormatEtc Interface, das wir zur Implementierung des gerade betrachteten Interfaces brauchen.


Fangen wir doch mit letzterem an:
ausblenden Delphi-Quelltext
1:
TMyEnum = class(IEnumFormatEtc)					


Dieses Objekt ist für den Zugriff auf unser FormatEtc da. Dabei zeigt die interne Variable Index den Index des gerade zu betrachtenden Elements an.

QueryInterface, AddRef und Release wieder wie eben bei TMyDataObject.

Skip überspringt die angegebene Anzahl von Einträgen.

Reset setzt den Index auf das erste Element zurück, sodass man von vorne durch das Array gehen kann.

Clone erzeugt ein identisches Objekt zu dem eigenen.

Nun fehlt noch next:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
function TMyEnum.Next(celt: Longint; var elt;
  pceltFetched: PLongint): HResult; stdcall;
begin
  Result := S_FALSE;
  if (Index = 0and (celt > 0then begin
    Inc(Index);
    with TFormatEtc(elt) do begin
      cfFormat := CF_HDROP;
      ptd := nil// not sure I should do this!
      dwAspect := DVASPECT_CONTENT;
      lindex := -1;
      tymed := TYMED_HGLOBAL;
    end;

    if pceltFetched <> nil then pceltFetched^ := 1;
    if celt = 1 then Result := S_OK;
  end else begin
    if pceltFetched <> nil then pceltFetched^ := 0;
  end;
end;

Die übergebene Variable elt bekommt die Werte des Elements des Arrays.


Wichtig ist jetzt noch TMyDropSource, abgeleitet von IDropSource:
ausblenden Delphi-Quelltext
1:
TMyDropSource = class(IDropSource)					


QueryInterface, AddRef und Release wieder wie oben bei TMyDataObject.

Im Konstruktor wird die Instanz von der Komponente TDragFilesSrc übergeben, die benutzt wird.

Fehlen noch zwei Methoden:
QueryContinueDrag und GiveFeedback

GiveFeedback ist schnell erklärt: Damit wird für den Benutzer ein visuelles Feedback über den aktuellen Operationsstatus angegeben, die dann vom Programm dem Benutzer angezeigt werden sollte.
In einer internen Variable merkt sich das Objekt den Status.

Nun aber zu QueryContinueDrag:
Darin wird die Fortsetzung des Drag-Vorgangs festgelegt oder festgestellt, dass die Dateien abgelegt wurden.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
function TMyDropSource.QueryContinueDrag(fEscapePressed: BOOL;
  grfKeyState: Longint): HResult; stdcall;
var
  drpEffect: integer;
begin
  if srcDFS.DropEffect = deCopy then drpEffect := DROPEFFECT_COPY
  else drpEffect := DROPEFFECT_MOVE;
  //Den entsprechenden Effekt einstellen (für Visualisierung für den
  //Benutzer)

  if fEscapePressed then Result := DRAGDROP_S_CANCEL
  //Wenn Escape gedrückt wurde, den Vorgang abbrechen
  else if (grfKeyState and MK_LBUTTON) = 0 then begin
    //Wenn die linke Maustaste nicht mehr gedrückt ist, wurden die Dateien
    //irgendwo abgelegt.
    if (srcDropEffect = drpEffect) and assigned( srcDFS.OnDropping ) then
      srcDFS.OnDropping(srcDFS);
    //Ggf. das Event auslösen, das in der Komponente zugewiesen werden kann,
    //für das Ereignis, dass die Dateien abgelegt werden.
    if srcDFS.FileCount = 0 then Result := DRAGDROP_S_CANCEL
    else Result := DRAGDROP_S_DROP;
    //Wenn keine Dateien vorhanden sind, Vorgang abbrechen, ansonsten
    //zurückgeben, dass die Dateien abgelegt wurden
  end else
    Result := S_OK;
    //Sonst Vorgang fortsetzen, d.h. die Dateien sind noch "in der Schwebe"
end;



Nun ja, und die Komponente ist relativ einfach zu verstehen. Im Konstruktor werden die Standardwerte gesetzt und die Dateiliste initialisiert.
Im Destruktor wird die Dateiliste wieder aus dem Speicher entfernt.

Außerdem kann eine oder mehrere Dateien hinzugefügt werden:
AddFile / AddFiles

Auch kann man die Anzahl der Dateien in der Liste erfragen:
GetFileCount

Am wichtigsten ist aber Execute:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
function TDragFilesSrc.Execute: TDragResult;
var i: integer;
    dwEffect: Longint;
    DropSource : TMyDropSource;
    Dataobject : TMyDataObject;
begin
  Result := drInvalid;
  //Falls etwas nicht geht (z.B. keine Dateien oder nicht existente
  //Dateien in der Liste), auf nicht geklappt setzen

  //Check that there are files in the list!
  if (fFileList.count = 0or (fFileList[0] = ''then exit;
  if fVerifyFiles then
    for i := 1 to fFileList.count do
      if not fileexists(fFileList[i-1]) then exit;
  //Das ist wohl klar: Wenn die Dateien auf Existenz überprüft werden 
  //sollen, alle Dateien durchgehen und mit FileExists überprüfen,
  //falls eine nicht existiert Methode beenden

  try
    DataObject:=TMyDataObject.create(fFileList);
    DataObject.AddRef;
    //Datenobjekt mit der Liste der Dateien initialisieren
    try
      DropSource:=TMyDropSource.create(self);
      DropSource.AddRef;
      //Note: DROPEFFECT_COPY = 1, DROPEFFECT_MOVE = 2
      //      hence the following is a crude typecast...
      //      DROPEFFECT := byte(fDropEffect)+1
      //      ie: deCopy -> DROPEFFECT_COPY, deMove -> DROPEFFECT_MOVE

      //DropSource initialisieren

      if (DoDragDrop(dataobject, dropsource, byte(fDropEffect)+1
        dwEffect) = DRAGDROP_S_DROP) and (dwEffect = byte(fDropEffect)+1
        then Result := drDropped
        //Wenn Ergebnis "gedroppt" ist und das richtige in Effect steht,
        //zurückgeben, dass die Dateien irgendwo abgelegt wurden
      else Result := drCancelled;
      //Ansonsten wurde der Vorgang abgebrochen
      DropSource.release;
      //Freigeben!!
    finally
      DataObject.release;
      //Freigeben!!
    end;
  except
  end;
  //try..except Blöscke sind bei OLE Operationen immer ratsam, wenn man 
  //keine vollständige Fehlerüberprüfung durchführt, und das geht kaum 
  //wirklich
end;



Nicht vergessen darf man OleInitialize und OleUninitialize vor bzw. nach einem Aufruf der OLE-Funktionen aufzurufen, da sonst ein Fehler auftritt!


Ich hoffe das hilft euch und die Komponenten sind euch nützlich.

Danke auch noch an Angus, der die Komponente entwickelt und zur Verfügung gestellt hat.
(Ich hab ja nur die Funktionsweise erläutert...)



Sebastian Jänicke


P.S.: Seid nachsichtig, das ist erst mein drittes Tutorial, vielleicht hab ich mich manchmal etwas unklar ausgedrückt...

Bei Fragen, fragt...


Zuletzt bearbeitet von jaenicke am Di 17.08.04 11:18, insgesamt 2-mal bearbeitet