C# 中拖动树节点






4.83/5 (68投票s)
2005年1月24日
4分钟阅读

404480

9672
本文介绍了如何在 C# 中实现类似资源管理器的树视图拖放功能。
引言
在开发一个 C# 应用程序的过程中,我遇到了一个问题,需要在树视图中添加拖放功能。到目前为止一切顺利,但为了让整个过程更炫酷一些,我决定像 Windows 资源管理器拖动文件或目录那样,也添加图像拖动功能。这时我遇到了麻烦,因为 .NET 控件本身不支持图像拖动。我在网上没有找到令人满意的代码(也许我不是一个好的冲浪者……),于是我决定自己尝试。在尝试的过程中,我通过添加滚动功能改进了代码,以便被拖动的元素可以放置在控件的任何位置。
本文将只简要介绍拖放的基础知识,因为实现起来相当直接,而且已经有很多关于它的优秀文章。本文的目的是介绍如何在 C# 中实现图像拖动和拖动时的自动滚动。
TreeView 拖放
要在 TreeView
上启用拖放,必须将 AllowDrop
标志设置为 true,并实现以下事件(或部分事件)的事件处理程序:
ItemDrag
- 此事件在拖动操作开始时触发。此事件是 listview 和 treeview 特有的。被拖动的元素作为事件的参数传递。此事件的处理程序应包含DoDragDrop()
调用,以开始拖放操作。DragOver
- 当用户将鼠标拖过支持拖放的控件时,此事件被触发。DragEnter
- 当用户在拖动元素时将鼠标移到控件上时,此事件被触发。DragLeave
- 当用户在拖动元素时将鼠标移出控件时,此事件被触发。DragDrop
- 当用户在放置目标上释放鼠标时,此事件被触发。GiveFeedback
- 此事件提供有关当前拖动效果和光标的反馈。
图像拖动
图像拖动的实现需要一些功能,首先是创建一个被拖动元素的“幽灵”图像,然后当鼠标光标移动到 TreeView
控件上时,移动该图像。一些所需的功能在 Win32(WinAPI)的 ImageList
实现中可用。为了调用这些函数,我编写了 DragHelper
类,它通过 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, // Handler of the image list containing the image to drag
int iTrack, // Index of the image to drag
int dxHotspot, // x-delta between mouse position and drag image
int dyHotspot // y-delta between mouse position and drag image
);
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_DragMove(
int x, // X-coordinate (relative to the form,
// not the treeview) at which to display the drag image.
int y, // Y-coordinate (relative to the form,
// not the treeview) at which to display the drag image.
);
[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, // Handle to the control that owns the drag image.
int x, // X-coordinate (relative to the treeview)
// at which to display the drag image.
int y // Y-coordinate (relative to the treeview)
// at which to display the drag image.
);
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_DragLeave(
IntPtr hwndLock // Handle to the control that owns the drag image.
);
[DllImport("comctl32.dll", CharSet=CharSet.Auto)]
public static extern bool ImageList_DragShowNolock(
bool fShow // False to hide, true to show the image
);
static DragHelper()
{
InitCommonControls();
}
}
开始拖动元素时要做的第一件事是创建树节点的“幽灵”图像。ImageList_BeginDrag
函数提供了帮助,该函数为我们创建一个“幽灵”图像。此函数需要一个 ImageList
的句柄作为参数,其中包含要使其透明的图像。为了创建要拖动的树节点的图像,会创建一个新的位图,并在其中绘制图标和标签。在拖动操作结束时,通过调用 ImageList_EndDrag
函数来销毁“幽灵”图像。我们在 ItemDrag
事件处理程序中完成所有这些操作。
private void treeView_ItemDrag(object sender,
System.Windows.Forms.ItemDragEventArgs e)
{
// Get drag node and select it
this.dragNode = (TreeNode)e.Item;
this.treeView1.SelectedNode = this.dragNode;
// Reset image list used for drag image
this.imageListDrag.Images.Clear();
this.imageListDrag.ImageSize =
new Size(this.dragNode.Bounds.Size.Width
+ this.treeView1.Indent, this.dragNode.Bounds.Height);
// Create new bitmap
// This bitmap will contain the tree node image to be dragged
Bitmap bmp = new Bitmap(this.dragNode.Bounds.Width
+ this.treeView1.Indent, this.dragNode.Bounds.Height);
// Get graphics from bitmap
Graphics gfx = Graphics.FromImage(bmp);
// Draw node icon into the bitmap
gfx.DrawImage(this.imageListTreeView.Images[0], 0, 0);
// Draw node label into bitmap
gfx.DrawString(this.dragNode.Text,
this.treeView1.Font,
new SolidBrush(this.treeView1.ForeColor),
(float)this.treeView1.Indent, 1.0f);
// Add bitmap to imagelist
this.imageListDrag.Images.Add(bmp);
// Get mouse position in client coordinates
Point p = this.treeView1.PointToClient(Control.MousePosition);
// Compute delta between mouse position and node bounds
int dx = p.X + this.treeView1.Indent - this.dragNode.Bounds.Left;
int dy = p.Y - this.dragNode.Bounds.Top;
// Begin dragging image
if (DragHelper.ImageList_BeginDrag(this.imageListDrag.Handle, 0, dx, dy))
{
// Begin dragging
this.treeView1.DoDragDrop(bmp, DragDropEffects.Move);
// End dragging image
DragHelper.ImageList_EndDrag();
}
}
当鼠标在拖动树节点时移动时,“幽灵”图像应该跟随鼠标光标。这可以通过 ImageList_DragMove
函数来实现。我们在 DragOver
事件处理程序中实现它。
private void treeView1_DragOver(object sender,
System.Windows.Forms.DragEventArgs e)
{
// Compute drag position and move image
Point formP = this.PointToClient(new Point(e.X, e.Y));
DragHelper.ImageList_DragMove(formP.X - this.treeView1.Left,
formP.Y - this.treeView1.Top);
...
}
如果我们移出 TreeView
,“幽灵”图像应该消失,并且一旦我们重新进入控件,图像应该再次出现。这可以通过 ImageList_DragLeave
和 ImageList_DragEnter
函数来实现。ImageList_DragEnter
函数还会锁定窗口进行更新,以允许图像的干净拖动。ImageList_DragLeave
分别解除更新锁定。我们在相应的事件处理程序(treeView1_DragEnter
和 treeView1_DragLeave
)中实现这两个函数。
在拖动元素时,Windows 会根据拖动效果(复制、移动、无等)自动更改鼠标光标。在我们的示例中,我们使用 DragDropEffects.Move
拖动效果。我们希望在拖动“幽灵”图像时使用的鼠标光标是正常的指针光标。可以在 GiveFeedback
事件处理程序中设置光标。
private void treeView1_GiveFeedback(object sender,
System.Windows.Forms.GiveFeedbackEventArgs e)
{
if(e.Effect == DragDropEffects.Move)
{
// Show pointer cursor while dragging
e.UseDefaultCursors = false;
this.treeView1.Cursor = Cursors.Default;
}
else e.UseDefaultCursors = true;
}
一旦用户放置了元素,从而终止了拖动操作,就需要调用 ImageList_DragLeave
函数解锁控件的更新。DoDragDrop()
调用(参见 treeView1_ItemDrag
)结束,并且使用 ImageList_EndDrag
释放“幽灵”图像。
private void treeView1_DragDrop(object sender,
System.Windows.Forms.DragEventArgs e)
{
// Unlock updates
DragHelper.ImageList_DragLeave(this.treeView1.Handle);
...
}
}
拖动时滚动
如果没有滚动,我们在拖动元素时将无法到达树中的每个节点,除非树完全显示在屏幕上。当鼠标光标到达 TreeView
控件的顶部或底部时,我们开始滚动控件。滚动通过 TreeNode
类的 EnsureVisible()
方法实现。要向上滚动,我们通过 TreeNode
类的 PrevVisibleNode
属性获取前一个可见节点,并使其可见。类似地,我们使用 NextVisibleNode
属性和 EnsureVisible()
调用向下滚动。一旦我们开始拖动一个节点,就会启动一个计时器。在计时器的每次滴答时,会检查光标的当前位置,如果它靠近控件的上边框或下边框,则会滚动树。为了避免拖动图像干扰滚动产生难看的图形效果,我们会暂时隐藏拖动图像并使用 ImageList_DragShowNolock
函数解锁绘图更新。就是这样!
private void timer_Tick(object sender, EventArgs e)
{
// get node at mouse position
Point pt = PointToClient(Control.MousePosition);
TreeNode node = this.treeView1.GetNodeAt(pt);
if(node == null) return;
// if mouse is near to the top, scroll up
if(pt.Y < 30)
{
// set actual node to the upper one
if (node.PrevVisibleNode!= null)
{
node = node.PrevVisibleNode;
// hide drag image
DragHelper.ImageList_DragShowNolock(false);
// scroll and refresh
node.EnsureVisible();
this.treeView1.Refresh();
// show drag image
DragHelper.ImageList_DragShowNolock(true);
}
}
// if mouse is near to the bottom, scroll down
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);
}
}
}