|
|
|
|
Programmieren mit C++Borland C++32-Bit-ProgrammierungDrag&Drop mit TreeViews
|
Wie wird Drag&Drop mit TreeView Controls realisiert, damit einzelne Einträge oder Teilbäume in der Control verschoben werden können?
| Frage | |
Voraussetzung für Drag&Drop mit einer TreeView Control ist, daß der Fensterstil TVS_DISABLEDRAGDROP nicht explizit gesetzt ist, um Drag&Drop zu unterbinden. Ist der Fensterstil gesetzt, reagiert die TreeView erst gar nicht auf Drag&Drop-Versuche des Anwenders und erzeugt keinerlei Nachrichten.
Ist Drag&Drop nicht untersagt, so startet die TreeView den Ziehvorgang automatisch, indem sie eine Benachrichtigung an das Elternfenster sendet. Dabei sind zwei Nachrichten möglich:
|
Konstante
|
Bedeutung
|
|
TVN_BEGINDRAG
|
Ziehen mit linker Maustaste
|
|
TVN_BEGINRDRAG
|
Ziehen mit rechter Maustaste
|
In beiden Fällen ist jedoch das weitere Vorgehen identisch, sofern keine Unterscheidung zwischen den beiden Tasten erfolgt, was jedoch nur in Bezug auf die Verarbeitung der gedroppten Daten von Bedeutung wäre. Beide Nachrichten verweisen auch über ihren Parameter lParam auf eine NM_TREEVIEW Struktur, deren Member hdr den Nachrichtencode und die ID der absendenden Control beinhaltet. Das Member itemOld enthält keine Daten, denn alle Informationen über den vom Anwender gezogenen Eintrag stehen in itemNew, wobei jedoch nur hItem, state und lParam von Interesse sind.
Das Member ptDrag der NM_TREEVIEW Struktur ist mit dem Bildpunkt initialisiert, auf den der Anwender zu Beginn der Drag-Operation geklickt hat. Der Punkt liegt auf jeden Fall im umgebenden Rechteck des in itemNew angegebenen Eintrags und ist relativ zur oberen linken Ecke des Clientbereichs ausgedrückt.
| Lösung | |
Sinnvollerweise werden die Daten des itemNew Eintrags oder gleich die ganze NM_TREEVIEW Struktur in einer statischen Variable zwischengespeichert, da weitere Nachrichten beim Elternfenster eintreffen werden, bevor die Drag-Operation beendet ist.
| Daten zwischenspeichern | |
Als erste Reaktion auf das Eintreffen der Nachricht TVN_BEGINDRAG muß ein Drag-Image erzeugt werden, das während des Ziehens als Symbol angezeigt wird. Die TreeView Control kennt hierfür die spezielle Nachricht TVM_CREATEDRAGIMAGE, die einen Parameter benötigt.
| Drag-Image definieren |
wParam = 0;
lParam = (LPARAM) (HTREEITEM) hItem;
Das korrespondierende Makro lautet:
HIMAGELIST TreeView_CreateDragImage(
hwnd, hItem);
Der Parameter hItem übergibt das Handle des Eintrags, dem ein neues Drag-Symbol zugewiesen werden soll. Als Ergebnis liefert die Nachricht ein Handle auf die ImageList, der das Drag-Image hinzugefügt wurde oder NULL, wenn etwas schiefgelaufen ist. Die gelieferte ImageList enthält lediglich ein Bild, das aus dem Symbol und dem Text des Eintrags besteht. Dieses wird während des Ziehens ständig dem Mauszeiger nachgeführt, um den Ziehvorgang sichtbar zu machen.
Gemäß den Konventionen der ImageList Control ist das gelieferte Drag-Image der ImageList zunächst als Drag-Image bekanntzumachen, bevor es benutzt werden kann. Dazu dient die Funktion ImageList_BeginDrag(). Als Hotspot sollte 0 eingesetzt werden, da die exakten relativen Koordinaten eines TreeView-Eintrags schwer zu ermitteln sind. Auch der Explorer setzt einfach (0, 0) als Hotspot.
Für den Code zur Reaktion auf TVN_BEGINDRAG werden einige statische Variablen benötigt.
static BOOL bDragging; // Wird ein Eintrag verschoben?
static HWND hTreeView; // Handle des TreeViews
static HTREEITEM hDragItem; // zu verschiebender Eintrag
static HTREEITEM hDropTarget; // Ziel-Eintrag
static HIMAGELIST hDragImage; // Bild für Drag-Operation
Der Code für das Erzeugen des Drag-Image benötigt zunächst einige einleitende Aufrufe, bevor die eigentliche Arbeit in Angriff genommen werden kann.
switch( wMsg )
{
case WM_NOTIFY:
switch( LOWORD( wp ) )
{
case IDC_TREEVIEW:
{
LPNM_TREEVIEW pnmtv = (LPNM_TREEVIEW)lp;
switch( pnmtv->hdr.code)
{
case TVN_BEGINDRAG:
{
POINT pt;
hDragImage = TreeView_CreateDragImage(
hTreeView, pnmtv->itemNew.hItem );
if (!ImageList_BeginDrag(hDragImage, 0, 0, 0 ) )
return FALSE;
Nun kann der eigene Cursor ersetzt und der Systemcursor ausgeschaltet werden.
ImageList_SetDragCursorImage(hDragCursors, 0, -10, -10);
ShowCursor( FALSE );
Als Hotspot ist hier (10, 10) gesetzt worden. Dies sind individuelle Werte, die abhängig vom tatsächlich benutzten Cursor abweichen können. Sinnvolle Werte wären auch (0, 0).
Die aktuelle Cursorposition ergibt sich aus ptDrag, die zunächst in Bildschirmkoordinaten umgerechnet wird, bevor ImageList_DragEnter() die Drag-Operation einleitet.
pt.x = pnmtv->ptDrag.x;
pt.y = pnmtv->ptDrag.y;
ClientToScreen( hTreeView, &pt );
ImageList_DragEnter(NULL,pt.x,pt.y);
| Nachricht TVM_CREATEDRAGIMAGE | |
Die Eingabe von NULL als Wert für den Parameter hwndLock in ImageList_DragEnter() hat zur Folge, daß der gesamte Desktop als Fenster für die Ausführung der Drag-Operation betrachtet wird. Da die Cursorposition auf Bildschirmkoordinaten konvertiert wurde, stimmen beide Einheiten überein, da die linke obere Fensterecke nunmehr die linke obere Ecke des Desktop ist.
Abschließend erfolgt das Einfangen der Maus über die API-Funktion SetCapture(), die alle Mauseingaben an das in hWnd spezifizierte Fenster leitet, sowie das Initialisieren interner Variablen.
| Drag-Fenster |
SetCapture( hWnd );
bDragging = TRUE;
hDropTarget = 0;
hDragItem = pnmtv->itemNew.hItem;
}
Allerdings genügt es nicht, daß Drag-Image nur zu erzeugen. Während des gesamten Drag-Prozesses muß man das Drag-Image dem Mauscursor nachführen. Dazu treffen bei der Fensterfunktion WM_MOUSEMOVE-Nachrichten ein, bis der Drag-Vorgang durch Loslassen der jeweils auslösenden Taste beendet wird und dementsprechend entweder eine WM_LBUTTONUP oder WM_RBUTTONUP Nachricht seitens Windows API resultiert.
| Abschließende Initialisierungen | |
Darüber hinaus muß die Anwendung die unter dem Mauscursor liegenden Objekte als potentielle Drop-Ziele darstellen, indem die betroffenen Einträge entsprechend anders gezeichnet werden. Die TreeView Control kennt dafür das Style-Flag TVIS_DROPHILITED, das im Member state einer TV_ITEM Struktur via Nachricht TVM_SETITEM im jeweiligen Eintrag gesetzt werden kann.
| Drop-Ziel kennzeichnen | |
Ist das Ziehen auf beliebige Fenster erlaubt, können die notwendigen Eingriffe recht umfangreich werden. Beschränkt man sich darauf, daß nur Einträge des aktuellen TreeViews als Drop-Ziel in Frage kommen, wird die Sache etwas einfacher, da die Funktion ImageList_DragMove() alle erforderlichen Operationen veranlaßt. Allerdings ist darauf zu achten, daß der Stil TVIS_DROPHILITED korrekt gesetzt wird, da Mausbewegungen dazu führen können, daß sich der darunterliegende Eintrag ändert, so daß der bislang markierte Eintrag wieder im ursprünglichen Zustand gezeichnet werden muß. Der Test, ob sich der Mauscursor noch über dem aktuellen Eintrag befindet, kann über die Nachricht TVM_HITTEST erfolgen.
Dazu wird zunächst die aktuelle Mausposition und dann der Eintrag unter dem Mauscursor ermittelt.
case WM_MOUSEMOVE:
{
if( bDragging )
{
HTREEITEM hItem;
TV_HITTESTINFO tvhtst;
tvhtst.pt.x = LOWORD( lp );
tvhtst.pt.y = HIWORD( lp );
hItem = TreeView_HitTest(hTreeView,
&tvhtst);
In hItem wird der unter dem Mauscursor liegende Eintrag geliefert. Ist dies ein anderer als in hDropTarget gespeichert, hat sich der Eintrag geändert. In diesem Fall ist der alte Eintrag zu demarkieren und der neue wird markiert. Da diese Operation häufiger benötigt wird, ist der Code in die Funktion SetDropHilite() ausgelagert worden.
Vor dem Zeichnen muß das Drag-Image jedoch ausgeblendet werden, da ansonsten beim anschließenden Aufruf von ImageList_DragMove() der ursprüngliche Bildschirminhalt unter dem Bild wiederhergestellt würde, was jedoch falsch ist, da dessen Inhalt gerade geändert wird. Über die Funktion ImageList_DragShowNolock() kann ein Update unterdrückt werden, indem als Parameter FALSE übergeben wird. Nach dem Ändern des Statusflags muß dann ein weiterer Aufruf mit Parameter TRUE folgen.
| Drop-Ziel aktualisieren |
if( hItem != hDropTarget )
{
ImageList_DragShowNolock(FALSE);
SetDropHilite( hTreeView,
hDropTarget,
hItem );
hDropTarget = hItem;
ImageList_DragShowNolock(TRUE);
}
Danach wird das Drag-Image an die Maus-Position verschoben.
ClientToScreen( hTreeView, &tvhtst.pt);
ImageList_DragMove( tvhtst.pt.x, tvhtst.pt.y );
Das Aussehen des Cursors zeigt an, ob ein Ziehen auf das Drop-Ziel erlaubt ist.
ImageList_SetDragCursorImage( hDragCursors,
IsChildOf(hTreeView,
hDragItem,
hDropTarget ) ? 1 : 0,
-10, -10);
}
}
Die Funktion SetDropHilite() leistet das Austauschen der Statusflags, indem TVIS_DROPHILITED aus dem Eintrag hOldDropTarget gelöscht und in hNewDropTarget eingetragen wird. Abschließend erzwingt die Funktion ein Update des Fensters über die API-Funktion UpdateWindow().
| Drag-Ziel wechseln |
void SetDropHilite(HWND hTreeView,
HTREEITEM hOldDropTarget,
HTREEITEM hNewDropTarget )
{
if( hNewDropTarget != hOldDropTarget )
{
TV_ITEM tvi;
if( hNewDropTarget )
{
tvi.hItem = hNewDropTarget;
tvi.mask = TVIF_STATE;
tvi.state = TVIS_DROPHILITED;
tvi.stateMask = TVIS_DROPHILITED;
TreeView_SetItem( hTreeView, &tvi );
}
if( hOldDropTarget )
{
tvi.hItem = hOldDropTarget;
tvi.mask = TVIF_STATE;
tvi.state = 0;
tvi.stateMask = TVIS_DROPHILITED;
TreeView_SetItem( hTreeView, &tvi );
}
UpdateWindow( hTreeView );
}
}
| Funktion SetDropHilite | |
Das Ende des Drag-Vorgangs wird durch das Loslassen der linken bzw. rechten Maustaste erreicht, was zu einer entsprechenden API-Nachricht WM_LBUTTONUP bzw. WM_RBUTTONUP führt.
Über die zuvor erhaltenen WM_MOUSEMOVE Nachrichten ist das aktuelle Drop-Ziel bekannt und die Anwendung kann entsprechend reagieren, was im Detail jedoch vom konkreten Einzelfall abhängt.
Das Programm hat im Rahmen dieser Benachrichtigung das Drag-Image aus dem Fenster zu entfernen und den zugehörigen internen Puffer der ImageList wieder freizugeben, was über entsprechende Funktionen des ImageList-APIs erfolgt.
case WM_LBUTTONUP:
if ( bDragging )
{
ImageList_DragLeave(NULL);
ImageList_EndDrag();
Anschließend wird das Drag-Image zerstört und das Drop-Ziel wieder normal dargestellt.
ImageList_Destroy( hDragImage );
SetDropHighlight( hTreeView,
NULL,
hDropTarget );
Liegt ein gültiges Drop-Ziel vor und darf der Teilbaum an das Ziel verschoben werden, kopiert anschließend die Hilfsfunktion CopyTree() den zu verschiebenden Teilbaum an den Zieleintrag und das Original wird abschließend gelöscht. Voraussetzung hierfür ist, daß der Zieleintrag nicht im Ausgangs-Teilbaum enthalten ist, was die Hilfsfunktion IsChild Of() klärt.
| Drag-Vorgang beenden |
if ( hDropTarget )
{
if( !IsChildOf( hTreeView,
hDragItem, // Quelle
hDropTarget )) // Ziel
{
CopyTree( hTreeView,
hDropTarget, // Ziel
hDragItem ); // Quelle
TreeView_DeleteItem( hTreeView,
hDragItem );
}
}
Abschließend wird das statische Flag bDragging auf FALSE gesetzt, das Mauscaptureing beendet und der Mauscursor wieder angezeigt.
| Teilbaum verschieben |
bDragging = FALSE;
ReleaseCapture();
ShowCursor( TRUE );
}
Die Vorgehensweise für die rechte Maustaste erfolgt analog, wobei ggfs. Änderungen im Verhalten des eigentlichen Verschiebevorgangs realisiert werden können, um den unterschiedlichen Tasten Rechnung zu tragen.
| Abschlußarbeiten | |
Wie wird auf Tastaturereignisse in der TreeView Control reagiert?
| Frage | |
Normalerweise muß man auf Tastaturereignisse gar nicht reagieren, da die TreeView Control alles automatisch regelt. Soll jedoch beispielsweise auf das Anklicken oder Doppelklicken eines Eintrags eine Reaktion erfolgen, muß die Eingabe beobachtet werden. Dazu sendet die TreeView Control allgemeine NM-Nachrichten, die im Rahmen der normalen Benachrichtigung eintreffen und im lParam einen Zeiger auf einen NMHDR übergeben. Dessen Member code enthält eine der folgenden Aktionscodes für Maus- und Tastatureingaben.
|
Konstante
|
Bedeutung
|
|
NM_CLICK
|
Mit linkem Mausbutton geklickt
|
|
NM_DBLCLK
|
Doppelklick mit linker Maustaste
|
|
NM_RCLICK
|
Mausklick mit rechter Taste innerhalb der Control
|
|
NM_RDBLCLK
|
Doppelklick in der Control mit rechter Maustaste
|
|
NM_RETURN
|
Der Anwender hat die Enter-Taste gedrückt, während die Control den Fokus besaß
|
| Lösung
|
|
|