Introduction
During the implementation of a C# application, I was faced with the problem of adding drag and drop functionality to a tree view. So far no problems, but in order to make the whole a bit fancier, I decided to add image dragging too, as it is done by the Windows Explorer when dragging files or directories. Here I got into troubles, since image dragging isn't supported by the .NET controls. Not finding satisfactory code in the Internet (maybe I'm not a good surfer...), I decided to try it by myself. Having fun on it, I improved the code by adding scrolling to the component so that a dragged element can be dropped everywhere on the control.
This article will only briefly describe the basics of drag and drop, as it is quite straightforward to implement and there already are a lot of good articles about it. The aim of the article is to describe how image dragging and automatic scrolling while dragging can be implemented in C#.
TreeView Drag and Drop
In order to allow Drag and Drop on a TreeView
, the AllowDrop
flag must be set and handlers for the the following events (or some of them) must be implemented:
ItemDrag
- This event is fired as soon as a drag operation is started. This event is specific for listviews and treeviews. The dragged element is passed as an argument of the event. The handler of this event should contain the DoDragDrop()
call that begins the drag and drop operation.
DragOver
- This event is fired when the user drags over a drag and drop control with the mouse.
DragEnter
- This event is fired when the user moves the mouse onto the control while dragging an element.
DragLeave
- This event is fired when the user leaves the control with the mouse while dragging an element.
DragDrop
- This event is fired when the user releases the mouse over the drop target.
GiveFeedback
- This event gives feedback about the current drag effect and cursor.
Image dragging
The implementation of image dragging requires functionalities that first of all create a ghost image of the dragging element and then move this image as mouse cursor moves over the TreeView
control. Part of the needed functionalities are available in the ImageList
implementation of Win32 (WinAPI). In order to call these functions, I wrote the class DragHelper
that accesses them via P/Invoke.
public class DragHelper
{
[DllImport("comctl32.dll")]
public static extern bool InitCommonControls();
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_BeginDrag(
IntPtr himlTrack,
int iTrack,
int dxHotspot,
int dyHotspot
);
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_DragMove(
int x,
int y,
);
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern void ImageList_EndDrag();
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_DragEnter(
IntPtr hwndLock,
int x,
int y
);
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_DragLeave(
IntPtr hwndLock
);
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_DragShowNolock(
bool fShow
);
static DragHelper()
{
InitCommonControls();
}
}
The first thing to do when we start dragging an element is to create the ghost image of the tree node. Help is provided by the function ImageList_BeginDrag
which creates for us a ghost image. This function needs as parameter the handler of an ImageList
with the image to be made transparent in it. To create the image of the tree node to drag, a new bitmap is created and the icon and the label are drawn in it. At the end of the dragging operation, the ghost image is destroyed by a call to the ImageList_EndDrag
function. We do all this in the ItemDrag
event handler.
private void treeView_ItemDrag(object sender,
System.Windows.Forms.ItemDragEventArgs e)
{
this.dragNode = (TreeNode)e.Item;
this.treeView1.SelectedNode = this.dragNode;
this.imageListDrag.Images.Clear();
this.imageListDrag.ImageSize =
new Size(this.dragNode.Bounds.Size.Width
+ this.treeView1.Indent, this.dragNode.Bounds.Height);
Bitmap bmp = new Bitmap(this.dragNode.Bounds.Width
+ this.treeView1.Indent, this.dragNode.Bounds.Height);
Graphics gfx = Graphics.FromImage(bmp);
gfx.DrawImage(this.imageListTreeView.Images[0], 0, 0);
gfx.DrawString(this.dragNode.Text,
this.treeView1.Font,
new SolidBrush(this.treeView1.ForeColor),
(float)this.treeView1.Indent, 1.0f);
this.imageListDrag.Images.Add(bmp);
Point p = this.treeView1.PointToClient(Control.MousePosition);
int dx = p.X + this.treeView1.Indent - this.dragNode.Bounds.Left;
int dy = p.Y - this.dragNode.Bounds.Top;
if (DragHelper.ImageList_BeginDrag(this.imageListDrag.Handle, 0, dx, dy))
{
this.treeView1.DoDragDrop(bmp, DragDropEffects.Move);
DragHelper.ImageList_EndDrag();
}
}
When the mouse is now moved while a tree node is dragged, the ghost image should follow the mouse cursor. This is done by the function ImageList_DragMove
. We implement it in the DragOver
event handler.
private void treeView1_DragOver(object sender,
System.Windows.Forms.DragEventArgs e)
{
Point formP = this.PointToClient(new Point(e.X, e.Y));
DragHelper.ImageList_DragMove(formP.X - this.treeView1.Left,
formP.Y - this.treeView1.Top);
...
}
If we leave the TreeView
, the ghost image should disappear, and as soon as we re-enter the control, the image should appear again. This is done by the functions ImageList_DragLeave
and ImageList_DragEnter
. The function ImageList_DragEnter
also locks the window for updates to allow a clean dragging of the image. ImageList_DragLeave
respectively releases the update lock. We implement these two functions in the corresponding event handlers (treeView1_DragEnter
and treeView1_DragLeave
).
While an element is dragged, Windows automatically changes the mouse cursor according to the drag effect (copy, move, none, ...). In our example, we use the DragDropEffects.Move
drag effect. The mouse cursor we want to have while dragging the ghost image is the normal pointer cursor. The cursor can be set in the GiveFeedback
event handler.
private void treeView1_GiveFeedback(object sender,
System.Windows.Forms.GiveFeedbackEventArgs e)
{
if(e.Effect == DragDropEffects.Move)
{
e.UseDefaultCursors = false;
this.treeView1.Cursor = Cursors.Default;
}
else e.UseDefaultCursors = true;
}
As soon as the user drops the element and thus terminates the dragging operation, the control updates must be unlocked calling the function ImageList_DragLeave
. The DoDragDrop()
call (see treeView1_ItemDrag
) terminates and the ghost image is released with ImageList_EndDrag
.
private void treeView1_DragDrop(object sender,
System.Windows.Forms.DragEventArgs e)
{
DragHelper.ImageList_DragLeave(this.treeView1.Handle);
...
}
}
Scrolling while dragging
Without scrolling, we can not reach each node within a tree while we are dragging an element, unless the tree is entirely visible on the screen. We begin scrolling the control if the mouse cursor reaches the top or the bottom of the TreeView
control. The scrolling is achieved through the EnsureVisible()
method of the TreeNode
class. To scroll up, we get the previous visible node through the property PrevVisibleNode
of the TreeNode
class, and set it visible. Analogously, we scroll down with the property NextVisibleNode
and a call to EnsureVisible()
. As soon as we begin dragging a node, a timer is started. At each tick of the timer, the current position of the cursor is checked, and if it is near the upper or lower border of the control, the tree is scrolled. To avoid that the dragging image interferes with the scrolling producing ugly graphical effects, we briefly hide the drag image and unlock the paint updates with the function ImageList_DragShowNolock
. That's it!
private void timer_Tick(object sender, EventArgs e)
{
Point pt = PointToClient(Control.MousePosition);
TreeNode node = this.treeView1.GetNodeAt(pt);
if(node == null) return;
if(pt.Y < 30)
{
if (node.PrevVisibleNode!= null)
{
node = node.PrevVisibleNode;
DragHelper.ImageList_DragShowNolock(false);
node.EnsureVisible();
this.treeView1.Refresh();
DragHelper.ImageList_DragShowNolock(true);
}
}
else if(pt.Y > this.treeView1.Size.Height - 30)
{
if (node.NextVisibleNode!= null)
{
node = node.NextVisibleNode;
DragHelper.ImageList_DragShowNolock(false);
node.EnsureVisible();
this.treeView1.Refresh();
DragHelper.ImageList_DragShowNolock(true);
}
}
}