[C++]: Drag & Drop auf eine Form

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!

Kommentare

  1. 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 ?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.