Wie schon vor einer ganzen Weile auf chris-blog.com angekündigt, zeige ich euch heute wie man in einer Windows-Anwendung Drag & Drop implementieren kann. Wie immer gilt: Ich verwende den C++ Builder von Embarcadero (in der Version XE6). Da es sich aber um Windows-Funktionen handelt sollte dies in anderen Compilern (mit kleineren Abweichungen) genauso funktionieren. Also legen wir los.
Drag & Drop auf eine Form
Als erstes habe ich eine neues VCL-Projekt erstellt. Auf der der Form muss als erstes Drag & Drop erlaubt werden und dann gleiche OLE initialisiert werden. Beim schließen muss OLE natürlich wieder deinitialisiert werden. Das ist ganz einfach und sind nur ein paar Zeilen Code. Hier der Konstruktor und Destruktor meiner Form:
__fastcall TFormMain::TFormMain(TComponent* Owner) : TForm(Owner) { // Drag & Drop "erlauben" DragAcceptFiles(this->Handle, true); // Sicherheitshalber Ole deinitialisieren OleUninitialize(); // Dann initalisieren if (OleInitialize(0) != S_OK) MessageBoxW(0, L"Die Ole-Initialisierung ist Fehlgeschlagen!", L"Achtung", MB_ICONERROR | MB_OK); } //--------------------------------------------------------------------------- __fastcall TFormMain::~TFormMain() { // Ole deinitialisieren OleUninitialize(); } //---------------------------------------------------------------------------
Als nächstes brauchen wir eine neue Klasse (Unit). Diese wird vom Interface IDropTarget abgeleitet. Die Header mit den entsprechenden Funktionen die benötigt werden sieht folgendermaßen aus:
#ifndef MyDropTargetH #define MyDropTargetH //--------------------------------------------------------------------------- class MyDropTarget : public IDropTarget { private: // Daten LONG m_lRefCount; HWND m_hWnd; bool m_bDropErlaubt; bool m_bVirtuell; DWORD m_dwDropEffekt; IDataObject *m_pDataObject; // Funktionen DWORD GetDropEffekt(DWORD dwKeyState, POINTL pt, DWORD dwErlaubt); bool QueryDataObject(IDataObject *pDataObject); void DropData(HWND hwnd, IDataObject *pDataObj); public: // Konstruktor + Destruktor MyDropTarget(HWND hwnd); ~MyDropTarget(); // IUnknown implementation HRESULT __stdcall QueryInterface (REFIID iid, void ** ppvObject); ULONG __stdcall AddRef (void); ULONG __stdcall Release (void); // Funktionen HRESULT __stdcall DragEnter (IDataObject * pDataObject, DWORD grfKeyState, POINTL pt, DWORD * pdwEffect); HRESULT __stdcall DragOver (DWORD grfKeyState, POINTL pt, DWORD * pdwEffect); HRESULT __stdcall DragLeave (void); HRESULT __stdcall Drop (IDataObject * pDataObject, DWORD grfKeyState, POINTL pt, DWORD * pdwEffect); }; //--------------------------------------------------------------------------- #endif
Die Header-Datei sieht jetzt zwar noch ziemlich wild aus, wenn ihr euch die folgenden Funktionen aber nach und nach anseht, wird aber schnell klar wozu die Funktionen und Variablen gut sind.
Als erstes der Konstruktor. Dazu brauch ich wohl nicht recht viel zu sagen. Hier werden einfach die Variablen initialisiert:
MyDropTarget::MyDropTarget(HWND hWnd) { this->m_lRefCount = 1; this->m_hWnd = hwnd; this->m_bAllowDrop = false; this->m_bVirtuell = false; } //---------------------------------------------------------------------------
Als nächstes sehen wir uns die Funktion DragEnter an. Dabei wird mit der Funktion QueryDataObject als erstes geprüft ob das entsprechende Objekt überhaupt auf die Form „gedropt“ werden darf. Danach wird in Abhängigkeit der Shift-/Strg-Taste geprüft ob das Objekt kopiert oder verschoben werden soll.
HRESULT __stdcall MyDropTarget::DragEnter(IDataObject * pDataObject, DWORD dwKeyState, POINTL pt, DWORD *pDropEffekt) { // Handelt es sich um Daten die wir annehmen wollen? this->m_bDropErlaubt = QueryDataObject(pDataObject); // Falls ja if (this->m_bDropErlaubt) { // Ermitteln um welchen "Effekt" (kopieren, verschieben, ...) es sich handelt *pDropEffekt = this->GetDropEffekt(dwKeyState, pt, *pDropEffekt); } else { *pDropEffekt = DROPEFFECT_NONE; } return S_OK; } //---------------------------------------------------------------------------
Wie schon erwähnt wird mit der Funktion QueryDataObject geprüft ob wir die entsprechende Datei annehmen. Außerdem ist es noch wichtig zu unterscheiden ob es sich um „virtuelle“ oder normale Dateien handelt. Normale Dateien sind z. B. .txt-Dateien, .iso-Datein, usw., „virtuelle“ Dateien sind z. B. E-Mails, die man direkt aus Outlook in das eigene Programm zieht.
bool MyDropTarget::QueryDataObject(IDataObject *pDataObject) { FORMATETC descriptor = {RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL}; FORMATETC contents = {RegisterClipboardFormat(CFSTR_FILECONTENTS), 0, DVASPECT_CONTENT, -1, TYMED_ISTREAM}; FORMATETC drop = {CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL}; // Handelt es sich um Dateien (normal oder Virtuell?) if ((pDataObject->QueryGetData(&descriptor) == S_OK && pDataObject->QueryGetData(&contents) == S_OK) || pDataObject->QueryGetData(&drop) == S_OK) { // drop (CF_HDROP) sind normale Dateien = z. B. .txt-Datei, usw., CFSTR_FILECONTENTS sind "virtuelle" Dateien = z. B. E-Mails aus Outlook if (pDataObject->QueryGetData(&drop) == S_OK) this->m_bVirtuell = false; else this->m_bVirtuell = true; return true; } else return false; } //---------------------------------------------------------------------------
Und hier gleich die Funktion mit der geprüft wird ob die Datei verschoben oder kopiert werden soll:
DWORD MyDropTarget::GetDropEffekt(DWORD dwKeyState, POINTL pt, DWORD dwErlaubt) { DWORD dwDropEffekt = 0; // Mit "pt" koennten wir pruefen ob wir das "Droppen" an den entsprechenden Koordinaten erlauben // Ueber "dwKeyState" ermitteln welcher "DropEffekt" ausgefuehrt werden soll if (dwKeyState & MK_CONTROL) // Strg-Taste = kopieren dwDropEffekt = dwErlaubt & DROPEFFECT_COPY; else if (dwKeyState & MK_SHIFT) // Shift-Taste = verschieben dwDropEffekt = dwErlaubt & DROPEFFECT_MOVE; // Falls keine Taste gedrueckt wird den "Standard" Dropeffekt verwenden if (dwDropEffekt == 0) { if (dwErlaubt & DROPEFFECT_COPY) dwDropEffekt = DROPEFFECT_COPY; if (dwErlaubt & DROPEFFECT_MOVE) dwDropEffekt = DROPEFFECT_MOVE; } return dwDropEffekt; } //---------------------------------------------------------------------------
Damit ist ein Großteil schon geschafft. Hier noch die Funktionen DragOver und DragLeave zu denen ich nicht viel sagen muss:
HRESULT __stdcall MyDropTarget::DragOver(DWORD dwKeyState, POINTL pt, DWORD *pDropEffekt) { if (this->m_bDropErlaubt) { *pDropEffekt = this->GetDropEffekt(dwKeyState, pt, *pDropEffekt); PositionCursor(m_hWnd, pt); } else { *pDropEffekt = DROPEFFECT_NONE; } return S_OK; } //--------------------------------------------------------------------------- HRESULT __stdcall MyDropTarget::DragLeave(void) { return S_OK; } //---------------------------------------------------------------------------
Die vorletzte Funktion die ich Zeige ist die Drop-Funktion. Diese bestimmt welche Aktion ausgeführt werden soll.
HRESULT __stdcall MyDropTarget::Drop(IDataObject *pDataObject, DWORD dwKeyState, POINTL pt, DWORD *pDropEffekt) { // PositionCursor(m_hWnd, pt); if (this->m_bDropErlaubt) { DropData(m_hWnd, pDataObject); *pDropEffekt = this->GetDropEffekt(dwKeyState, pt, *pDropEffekt); } else { *pDropEffekt = DROPEFFECT_NONE; } return S_OK; } //---------------------------------------------------------------------------
Wie man sieht, macht auch diese Funktion nicht sehr viel. Im Grunde ruft sie nur die Funktion DropData auf. Dies ist nun die größte und „schwierigste“ Funktion. Danach ist es aber geschafft.
Als erstes sehen wir uns an wie man normale Dateien verschieben und kopieren kann. Nachdem geprüft wurde ob überhaupt eine Datei übergeben wurde und ob dies eine normale Datei ist, muss die Datei in eine STGMEDIUM-Struktur gelegt werden. Falls das geklappt hat, wird der hGlobal-Teil von STGMEDIUM in eine HDROP-Variable gecastet.
Danach muss nur noch über DragQueryFile die Anzahl der Dateien ermittelt werden und in einer Schleife dann wieder per DragQueryFile der Dateiname. Dann einfach noch prüfen ob verschoben oder kopiert werden soll und schon kann man die Aktion starten:
// Pruefen ob es sich um "normale" Dateien handelt if (pDataObject != NULL && this->m_bVirtuell == false) { // Struct initialisieren STGMEDIUM storage = {0, 0, 0}; // Daten in die storage-Struct legen hr = pDataObject->GetData(&drop, &storage); // Nur falls das geklappt hat if (hr == S_OK) { // In HDROP casten HDROP hdrop = (HDROP)GlobalLock(storage.hGlobal); // Anzahl der Dateien ermitteln nAnzahlDateien = DragQueryFile(hdrop, -1, NULL, 0); // Anzahl der Dateien for(int i = 0; i < nAnzahlDateien; ++i) { // Laenge Dateiname nLength = DragQueryFile(hdrop, i, NULL, 0); // Dateinamen ermitteln DragQueryFileW(hdrop, i, caFileName, nLength + 1); // Soll verschoben/kopiert werden? if (this->m_dwDropEffekt == DROPEFFECT_COPY) // -> kopieren CopyFileW(caFileName, (".\\" + ExtractFileName(caFileName)).c_str(), false); // Die Datei kopieren, falls sie bereits existiert -> ueberschreiben else MoveFileW(caFileName, (".\\" + ExtractFileName(caFileName).c_str()); // -> verschieben } } }
Etwas schwieriger wirds da schon wenn es sich um eine „virtuelle“ Datei handelt, aber auch dass ist machbar. Dabei werden alle übergebenen „virtuellen“-Dateien durchgelaufen und geschaut ob es sich um eine Email oder um einen Anhang handelt. Je nachdem wird dann (für eine Mail) über ein IStorage-Objekt gespeichert und für Anhänge muss ein Umweg über ein Byte-Array und einen TMemoryStream gegangen werden. So sieht der Code aus:
// Struct initialisieren STGMEDIUM storage = {0, 0, 0}; // Daten in die storage-Struct legen (im "CFSTR_FILEDESCRIPTOR"-Format hr = this->m_pDataObject->GetData(&descriptor, &storage); // Nur falls das geklappt hat if (hr == S_OK) { // In FILEGROUPDESCRIPTOR casten file_group_descriptor = (::FILEGROUPDESCRIPTOR*)GlobalLock(storage.hGlobal); // Alle Dateien die eingefuegt werden sollen durchlaufen for(int i = 0; i < file_group_descriptor->cItems; ++i) { file_descriptor = file_group_descriptor->fgd[i]; contents.lindex = i; attachments.lindex = i; // Handelt es sich um einen IStorage? -> Mail if (this->m_pDataObject->GetData(&contents, &storage) == S_OK) { bMail = true; bAnhang = false; } // Handelt es sich um einen IStream? -> Anhang einer Mail else if (this->m_pDataObject->GetData(&attachments, &storage) == S_OK) { bMail = false; bAnhang = true; } // Den Dateinamen ermitteln if (bMail == true || bAnhang == true) { strDatei = file_descriptor.cFileName; } // Mail? if (bMail == true) { // IStorage *pStorage = storage.pstg; // Die Datei mit IStorage kopieren IStorage *myStorage; OleCheck(StgCreateDocfile((".\\" + strDatei).c_str(), STGM_CREATE | STGM_SHARE_EXCLUSIVE | STGM_DIRECT | STGM_READWRITE, 0, &myStorage)); pStorage->CopyTo(0, NULL, NULL, myStorage); myStorage->Release(); } // Anhang? else if (bAnhang == true) { STATSTG stg; // Stat (enthaelt groesse der Datei) auslesen hr = storage.pstm->Stat(&stg, STATFLAG_DEFAULT); if (hr == S_OK) { unsigned long uAnzahlBytesGelesen; // Neues ByteArray (LowPart = groesse der Datei) byte *b = new byte[stg.cbSize.LowPart]; // Neuen Stream erzeugen TMemoryStream *stream = new TMemoryStream(); // Den Stream in das Byte-Array schreiben storage.pstm->Read(b, stg.cbSize.LowPart, &uAnzahlBytesGelesen); // Die Bytes in den Stream schreiben stream->Write(b, uAnzahlBytesGelesen); // Den Stream speichern stream->SaveToFile(".\\"); // Freigeben delete stream; delete b; } } } } // Zuruecksetzen contents.lindex = -1; attachments.lindex = -1; GlobalUnlock(storage.hGlobal); // release the data using the COM API ReleaseStgMedium(&storage);
Das wars, nun ist die Form Drag & Drop-Fähig.
Hier Kann wie üblich das Fertige Projekt heruntergeladen werden.
Du hast Fragen, Anregungen, o. ä.? Ich freue mich wenn du einen Kommentar hinterlässt!
geht nicht
Naja, ein wenig mehr Info wäre schon nicht schlecht, wo hakt es denn genau? Welche Version nutzt du?
Hab das mal probiert wenn ich aber das Programm in Win 8.1 über den Totalcommander starte und den Drop über den WindowsExplorer durchführe funktioniert nix mehr, das Problem habe ich übrigens bei all meinen anderen Programmen auch. Woran liegt das ?
kann gelöscht werden, das Problem ist ein anderes
Hallo Chris, kannst du bitte dein komplettes Projekt nochmal veröffentlichen bzw mir schicken. Leider ist der Link nicht mehr aktive.
Hallo Manfred,
vor kurzem ist der Blog auf einen anderen Webspace umgezogen, deswegen funktionierten die Downloads nicht mehr. Ich habe das korrigiert, jetzt funkioniert alles wieder so wie es soll 🙂