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

拖动 Canvas 中的元素

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (10投票s)

2012年5月20日

CPOL

5分钟阅读

viewsIcon

51983

downloadIcon

3231

这是“拖动 Canvas 中的元素”的替代方案。

引言

在办公室进行 UI 开发期间,我遇到了一个需求,需要实现一个非常简单的 Canvas,能够在其内部及其周围拖动某些元素。

经过一些 Google 搜索,我找到了 John Smith 的文章 在 Canvas 中拖动元素,它的简洁性非常好,所以我决定采用它。正如标题所示,本文是关于自定义 WPF 的 Canvas 类,以便能够在其内拖动元素。

在浏览 DragCanvasDemo 的源代码并尝试使用时,发现一些功能并不完全适合我的目的,例如过多的静态 依赖项属性,一些未完全完成的边缘状态计算,例如如果不允许超出 Canvas 视图,则在元素返回之前,鼠标必须后退,就像溢出一样。最后一件事是,有太多的辅助方法进行了相同和重复的条件检查,因此移动元素的所有功能都在事件中发生,它应该尽可能小。最后,我在源代码中发现了一些用于将元素带到前面和发送到后面的功能,但它们从未在源代码的任何地方使用过。

尽管此 DragCanvas 源代码完全基于 John Smith 的 在 Canvas 中拖动元素,但仍然很难将两者进行比较,因为我做了一些更改。

背景

关于背景,请参阅 John Smith 的 在 Canvas 中拖动元素

使用代码

像使用常规 Canvas 一样使用 DragCanvas。唯一的区别是 DragCanvas 有一个附加属性 DraggingMode,它可以接受值:NoDraggingAllowDragInViewAllowDragOutOfView,根据 enum

//Defines the mode in which the DragCanvas will act
//if set to NoDragging will act a simple Canvas
//if set to AllowDragInView Dragging is enabled but no Draging out of Canvas view is allowed,
//elements will not get out of DragCanvas borders
//if set to AllowDragOutOfView dragging is enabled and elements may be dragged out of the
//DragCanvas borders
public enum DraggingModes
{
    NoDragging,
    AllowDragInView,
    AllowDragOutOfView
}

默认情况下,DragCanvas.Children 中的所有元素都是可拖动的。如果某些元素不能拖动,则将属性 DragCanvas:DragCanvas.CanBeDragged 设置为 False。请参阅下面的代码中 InfoBox1InfoBox2 之间的区别。

<DragCanvas:DragCanvas DraggingMode="AllowDragInView">

  <DragCanvas:InfoBox x:Name="InfoBox1" Canvas.Left="0" Canvas.Top="0" 
          DragCanvas:DragCanvas.CanBeDragged="True"/>

  <DragCanvas:InfoBox x:Name="InfoBox2" Canvas.Right="0" Canvas.Bottom="0" 
          DragCanvas:DragCanvas.CanBeDragged="False"/>
<DragCanvas:DragCanvas />

在 DragCanvas 源代码的 DragCanvas 命名空间中,有一个 internal enum 类,它定义了元素可以重新定位的方向。enum ModificationDirection 表示如下,并且构建为所有可能方向的 4 位掩码。这可能不是最高效的工作方式,但它确实有助于提高代码的可读性。如果您希望 enum 的所有实例都可以更改为 byte 并以相同方式使用。

//Defines to the DragCanvas Relative to which edges of it the element may be repositioned
internal enum ModificationDirection
{
    None                = 0,
    Left                = 1,
    Right               = 2,
    LeftRight           = 3,
    Top                 = 4,
    LeftTop             = 5,
    RightTop            = 6,
    LeftRightTop        = 7,
    Bottom              = 8,
    LeftBottom          = 9,
    RightBottom         = 10,
    LeftRightBottom     = 11,
    TopBottom           = 12,
    LeftTopBottom       = 13,
    RightTopBottom      = 14,
    LeftRightTopBottom  = 15
}

工作原理

DragCanvas 功能由 3 个阶段构建

阶段 1:初始化拖动

此阶段由派生类 Canvas 的重写方法 OnPreviewMouseLeftButtonDown 实现,该方法初始化拖动如下:

1)检查元素是否为 DragCanvas 的子元素。
2)检查 DependencyProperty "CanBeDragged" 是否设置为 True
3)如果上述 2 个条件为 True,则将此元素设置为 DraggedElement
4)检查相对于 DragCanvas 的哪个侧面放置了该元素(enum
    ModificationDirection)并设置元素的初始位置。
5)设置拖动正在进行的标志。
7)将元素带到前面。

protected override void OnPreviewMouseLeftButtonDown( MouseButtonEventArgs e )
{
    base.OnPreviewMouseLeftButtonDown( e ); // call the handler of the base first
    m_IsDragInProgress = false;
    m_InitialCursorLocation = e.GetPosition(this); // Cache the mouse cursor location

    //check if the mouse was clicked on the ScrollBar, in which case let the ScrollBar remain its control. 
    if (null != GetParentOfType<scrollbar>(e.OriginalSource as DependencyObject)) {  return; }

    // Walk up the visual tree from the element that was clicked, 
    // looking for an element that is a direct child of the Canvas
    DraggedElement = FindCanvasChild(e.Source as DependencyObject);
    if(m_DraggedElement == null) { return; }

    m_ModificationDirection = ResolveOffset(Canvas.GetLeft(m_DraggedElement), Canvas.GetRight(m    _DraggedElement), Canvas.GetTop(m_DraggedElement), Canvas.GetBottom(m_DraggedElement));

    // Set the Handled flag so that a control being dragged 
    // does not react to the mouse input
   e.Handled = true;

   if(m_ModificationDirection == ModificationDirection.None)
   {
       DraggedElement = null;
       return;
   }
   BringToFront(m_DraggedElement as UIElement);
   m_IsDragInProgress = true;
}

下一个方法 GetParentOfType 是一个辅助方法,它确定 MouseLeftButtonDown 事件的起源是否为 T 类型。它在 OnPreviewMouseLeftButtonDown 方法中使用,以确定鼠标是否单击了 ScrollViewScrollBar。如果是,则仅忽略该事件,让用户使用滚动条。

public static (DependencyObject current) where T : DependencyObject
{
    DependencyObject parent = current;
    do
    {
        if (parent is Visual)
        {
            parent = VisualTreeHelper.GetParent(parent);
        }
        else
        {
            parent = LogicalTreeHelper.GetParent(parent);
        }

        var result = parent as T;

        if (result != null)
        {
            return result;
        }
    } while (parent != null);

    return null;
}

在方法 ResolveOffset 中实际设置初始偏移量和 DragMode。

/// <summery>
/// Determines one component of a UIElement's location
/// within a Canvas (horizontal or vertical offset)
/// and according to it reletive to which sides it may be relocated
/// </summery>
/// <param name="Left">
/// The value of an offset relative to a Left side of the Canvas
/// </param>
/// <param name="Right">
/// The value of the offset relative to the Right side of the Canvas
/// </param>
/// <param name="Top">
/// The value of the offset relative to the Topside of the Canvas
/// </param>
/// <param name="Bottom">
/// The value of the offset relative to the Bottomside of the Canvas
/// </param>
private ModificationDirection ResolveOffset(double Left, double Right, double Top, double Bottom)
{
    // If the Canvas.Left and Canvas.Right attached properties 
    // are specified for an element, the 'Left' value is honored
    // The 'Top' value is honored if both Canvas.Top and 
    // Canvas.Bottom are set on the same element.  If one 
    // of those attached properties is not set on an element, 
    // the default value is Double.NaN.
    m_OriginalLefOffset = Left;
    m_OriginalRightOffset = Right;
    m_OriginalTopOffset = Top;
    m_OriginalBottomOffset = Bottom;
    ModificationDirection result = ModificationDirection.None;
    if (!Double.IsNaN(m_OriginalLefOffset))
    {
        result |= ModificationDirection.Left;
    }
    if (!Double.IsNaN(m_OriginalRightOffset))
    {
        result |= ModificationDirection.Right;
    }
    if (!Double.IsNaN(m_OriginalTopOffset))
    {
        result |= ModificationDirection.Top;
    }
    if (!Double.IsNaN(m_OriginalBottomOffset))
    {
        result |= ModificationDirection.Bottom;
    }
    return result;
}

FindCanvasChild 中进行单击元素是 DragCanvas 的子项的验证。

/// <summery>
/// looking for a UIElement which is a child of the Canvas.  If a suitable
/// element is not found, null is returned.  If the 'depObj' object is a
/// UIElement in the Canvas's Children collection, it will be returned.
/// </summery>
/// <param name="depObj">
/// A DependencyObject from which the search begins.
/// </param>
private UIElement FindCanvasChild(DependencyObject depObj)
{
    while (depObj != null)
    {
        // If the current object is a UIElement which is a child of the
        // Canvas, exit the loop and return it.
        UIElement elem = depObj as UIElement;
        if (elem != null && base.Children.Contains(elem))
        {
            break;
        }

        // VisualTreeHelper works with objects of type Visual or Visual3D
        // If the current object is not derived from Visual or Visual3D,
        // then use the LogicalTreeHelper to find the parent element.
        if (depObj is Visual || depObj is Visual3D)
        {
            depObj = VisualTreeHelper.GetParent(depObj);
        }
        else
        {
            depObj = LogicalTreeHelper.GetParent(depObj);
        }
    }
    return depObj as UIElement;
}

最后,通过方法 BringToFront 将元素带到前面,当然还有相反的方法 SendToBack,两者都使用相同的辅助方法 UpdateZOrder

/// <summery>
/// Assigns the element a z-index which will ensure that
/// it is in front of every other element in the Canvas.
/// The z-index of every element whose z-index is between
/// the element's old and new z-index will have its z-index
/// decremented by one.
/// </summery>
/// <param name="element">
/// The element to be sent to the front of the z-order.
/// </param>
public void BringToFront(UIElement element)
{
    UpdateZOrder(element, true);
}

/// <summery>
/// Assigns the element a z-index which will ensure that
/// it is behind every other element in the Canvas.
/// The z-index of every element whose z-index is between
/// the element's old and new z-index will have its z-index
/// incremented by one.
/// </summery>
/// <param name="element">
/// The element to be sent to the back of the z-order.
/// </param>
public void SendToBack(UIElement element)
{
    UpdateZOrder(element, fasle);
}

/// <summery>
/// Helper method used by the BringToFront and SendToBack methods.
/// </summery>
/// <param name="element">
/// The element to bring to the front or send to the back.
/// </param>
/// <param name="bringToFront">
/// Pass true if calling from BringToFront, else false.
/// </param>
private void UpdateZOrder(UIElement element, bool bringToFront)
{
        #region Safety Check

    if (element == null)
         throw new ArgumentNullException("element");

    if (!base.Children.Contains(element))
        throw new ArgumentException("Must be a child element of the Canvas.", "element");

        #endregion // Safety Check

        #region Calculate Z-Indexes And Offset

    // Determine the Z-Index for the target UIElement.
    int elementNewZIndex = -1;
    if (bringToFront)
    {
        foreach (UIElement elem in base.Children)
        {
            if (elem.Visibility != Visibility.Collapsed)
            {
                ++elementNewZIndex;
            }
        }
    }
    else
    {
        elementNewZIndex = 0;
    }

    // Determine if the other UIElements' Z-Index 
    // should be raised or lowered by one. 
    int offset = (elementNewZIndex == 0) ? +1 : -1;

    int elementCurrentZIndex = Canvas.GetZIndex(element);

        #endregion // Calculate Z-Indici And Offset

        #region Update Z-Indexes

    // Update the Z-Index of every UIElement in the Canvas.
    foreach (UIElement childElement in base.Children)
    {
        if (childElement == element)
        {
            Canvas.SetZIndex(element, elementNewZIndex);
        }
        else
        {
            int zIndex = Canvas.GetZIndex(childElement);

            // Only modify the z-index of an element if it is  
            // in between the target element's old and new z-index.
            if (bringToFront && elementCurrentZIndex < zIndex || !bringToFront && zIndex < elementCurrentZIndex)
            {
                Canvas.SetZIndex(childElement, zIndex + offset);
            }
        }
    }
        #endregion // Update Z-Indexes
}

现在我们已经检查并验证了我们的元素,将其带到前面并注册了初始偏移量,是时候开始移动它了。

阶段 2:计算和更新新的偏移量

所有计算魔术都发生在单个重写方法 OnPreviewMouseMove 中。这里没有太多需要解释的,这只是初中数学,但与 John Smith 的 在 Canvas 中拖动元素 的原始实现相比,这里有一些区别。其中一个非常明显的是,if 语句中存在一些代码重复,并且没有方法 CalculateDragElementRect。这些更改是为了防止一些计算开销。

此外,初始鼠标光标位置每次都会更新,这是为了修复当不允许拖动超出视图时出现的错误。当元素到达 DragCanvas 边界,而鼠标继续移出其边界时,该错误就会出现。在元素再次开始移动之前,需要将鼠标向后移动一段距离。

protected override void OnPreviewMouseMove(MouseEventArgs e)
{
    base.OnPreviewMouseMove(e);
    // If no element is being dragged, there is nothing to do.
    if (m_DraggedElement == null || !m_IsDragInProgress)
    {
        return;
    }
    // Get the position of the mouse cursor, relative to the Canvas.
    Point cursorLocation = e.GetPosition(this);

    // These values will store the new offsets of the drag element.
    double _HorizontalOffset = Double.NaN;
    double _VerticalOffset = Double.NaN; ;

    #region Calculate Offsets

    Size _DraggedElemenetSize = m_DraggedElement.RenderSize;

    if ((m_ModificationDirection & ModificationDirection.Left) != 0)
    {
        _HorizontalOffset = m_OriginalLefOffset + (cursorLocation.X - m_InitialCursorLocation.X);
        if (m_DraggingMode == DraggingModes.AllowDragInView)
        {
            if (_HorizontalOffset < 0)
            {
                _HorizontalOffset = 0;
            }
            else if ((_HorizontalOffset + _DraggedElemenetSize.Width) > this.ActualWidth)
            {
                _HorizontalOffset = this.ActualWidth - _DraggedElemenetSize.Width;
            }
        }
        m_OriginalLefOffset = _HorizontalOffset;
        m_OriginalRightOffset = this.ActualWidth + _DraggedElemenetSize.Width - m_OriginalLefOffset;
    }
    else if ((m_ModificationDirection & ModificationDirection.Right) != 0)
    {
        _HorizontalOffset = m_OriginalRightOffset - (cursorLocation.X - m_InitialCursorLocation.X);
        if (m_DraggingMode == DraggingModes.AllowDragInView)
        {
            if (_HorizontalOffset < 0)
            {
                _HorizontalOffset = 0;
            }
            else if ((_HorizontalOffset + _DraggedElemenetSize.Width) > this.ActualWidth)
            {
                _HorizontalOffset = this.ActualWidth - _DraggedElemenetSize.Width;
            }
        }
        m_OriginalRightOffset = _HorizontalOffset;
        m_OriginalLefOffset = this.ActualWidth + _DraggedElemenetSize.Width - m_OriginalRightOffset;
    }

    if ((m_ModificationDirection & ModificationDirection.Top) != 0)
    {
        _VerticalOffset = m_OriginalTopOffset + (cursorLocation.Y - m_InitialCursorLocation.Y);
        if (m_DraggingMode == DraggingModes.AllowDragInView)
        {
            if (_VerticalOffset < 0)
            {
                _VerticalOffset = 0;
            }
            else if ((_VerticalOffset + _DraggedElemenetSize.Height) > this.ActualHeight)
            {
                _VerticalOffset = this.ActualHeight - _DraggedElemenetSize.Height;
            }
        }
        m_OriginalTopOffset = _VerticalOffset;
        m_OriginalBottomOffset = this.ActualHeight + _DraggedElemenetSize.Height - m_OriginalTopOffset;
    }
    else if ((m_ModificationDirection & ModificationDirection.Bottom) != 0)
    {
        _VerticalOffset = m_OriginalBottomOffset - (cursorLocation.Y - m_InitialCursorLocation.Y);
        if (m_DraggingMode == DraggingModes.AllowDragInView)
        {
            if (_VerticalOffset < 0)
            {
                _VerticalOffset = 0;
            }
            else if ((_VerticalOffset + _DraggedElemenetSize.Height) > this.ActualHeight)
            {
                _VerticalOffset = this.ActualHeight - _DraggedElemenetSize.Height;
            }
        }
        m_OriginalBottomOffset = _VerticalOffset;
        m_OriginalTopOffset = this.ActualHeight + _DraggedElemenetSize.Height - m_OriginalBottomOffset;
    }

    m_InitialCursorLocation = cursorLocation;

    #endregion // Calculate Offsets

    #region Move Drag Element

    if ((m_ModificationDirection & ModificationDirection.Left) != 0)
    {
        Canvas.SetLeft(m_DraggedElement, _HorizontalOffset);
    }
    else if ((m_ModificationDirection & ModificationDirection.Right) != 0)
    {
        Canvas.SetRight(m_DraggedElement, _HorizontalOffset);
    }
    if ((m_ModificationDirection & ModificationDirection.Top) != 0)
    {
        Canvas.SetTop(m_DraggedElement, _VerticalOffset);
    }
    else if ((m_ModificationDirection & ModificationDirection.Bottom) != 0)
    {
        Canvas.SetBottom(m_DraggedElement, _VerticalOffset);
    }

    #endregion // Move Drag Element
}

阶段 3:完成拖动

对我们用户来说直观的是,当释放鼠标按钮时,元素就会被固定,现在即使鼠标继续移动,元素也会保持在原位。为了完成这项简单的任务,下一个 overriden 方法 OnPreviewMouseUp 可以完成任务,实际上它只是将拖动的元素置空并将标志 m_IsDragInProgress 设置为 false

protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
{
    base.OnPreviewMouseUp(e);

    // Reset the field whether the left or right mouse button was
    // released, in case a context menu was opened on the drag element.
    m_IsDragInProgress = false;
    DraggedElement = null;
}

可能的改进

与原始文章 在 Canvas 中拖动元素 中建议的 也许,公开一些与拖动相关的事件在某些场景下可能很有用,例如一个可取消的 BeforeElementDrag 事件,一个提供有关拖动元素及其位置信息的 ElementDrag 事件,以及一个 AfterElementDrag 事件。 一样,存在一些问题,例如,当拖动的元素是 TextBox 或包含 TextBoxBase 的派生类的 UserControl 时,则无法选择此类元素中的文本。
© . All rights reserved.