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 🙂