65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中拖动树节点

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (68投票s)

2005年1月24日

4分钟阅读

viewsIcon

404480

downloadIcon

9672

本文介绍了如何在 C# 中实现类似资源管理器的树视图拖放功能。

TreeView Drag & Drop

引言

在开发一个 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_DragLeaveImageList_DragEnter 函数来实现。ImageList_DragEnter 函数还会锁定窗口进行更新,以允许图像的干净拖动。ImageList_DragLeave 分别解除更新锁定。我们在相应的事件处理程序(treeView1_DragEntertreeView1_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);
        }
    } 
}
© . All rights reserved.