Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Dragging Elements in a Canvas

0.00/5 (No votes)
8 Jun 2012 1  
This is an alternative for Dragging Elements in a Canvas

Introduction

Well during some development of UI in the office I came across the need to Implement some stupid simple Canvas with the ability to drag some elements in it and around it.

after some Google search i came across the article of Dragging Elements in a Canvas by John Smith, and well it was quite good in its simplicity so i decided to adopt it. an well as the headline says this article is about customizing Canvas Class of WPF so it would be possible to drag a elements within it.

while going through the source of DragCanvasDemo and trying it out some, features were found not to entirely fit for my purposes, such as to much of static DependecyProperties, some not fully finished Calculation for edge states such as if no out of canvas view allowed the before the element will go beck, the mouse must go back as the over flow was. the last thing is that there was to much of helping methods that made same and repeated conditions checks and as that the whole functionality of moving an element happens during event it should be as small as possible and last i founded in the source some functions for bringing element to front and send them to the back, but they were never used in any point of the source.

although this DargCanvas source is entirely based on the Dragging Elements in a Canvas by John Smith, it still would be quite hard to compare the two since i made some changes

Background

for the Background see Dragging Elements in a Canvas by John Smith

Using the code

use DragCanvas as you would do the regular Canvas. Only difference is that DragCanvas has some additional property,  DraggingMode, and may receive values: NoDragging, AllowDragInView or AllowDragOutOfView according to the 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
}

By default all elements in the DragCanvas.Children are Draggable by default, if some elements should no be draggable the set the ptoperty DragCanvas:DragCanvas.CanBeDragged to False, see the differences in code bellow between InfoBox1 and InfoBox2

<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 />

In DragCanvas source, in it's namespace DragCanvas, is an internal enum class whitch defines the directions relative to which the element may be repositioned, the enum ModificationDirection represented below and is built as a 4 bit mask of all possible directions. It may be not the most sefficient way to work but it realy helps with the code readability. if you want all instances of the enum may be changed to byte and used same way.

//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
}

How It Works

DragCanvas functionallity built from 3 stages

Stage 1: Initialize Drag 

This Stage is Implemented by overriden method, OnPreviewMouseLeftButtonDown, of the derived class Canvas initializing the drag as folows:

1) check if the element is a child of DragCanvas
2) check if DependencyProperty "CanBeDragged" set to True
3) if 2 upper conditions are True then set this element as DraggedElement
4) check relative to what sides of DragCanvas this element is placed (the enum
    ModificationDirection) and set the initial location of the element
5) set the flag that the drag is in progress
7) bring the element to front

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;
}

the next method GetParentOfType is a helping method that determines if the originator of the MouseLeftButtonDown event is of type T, it is used in the OnPreviewMouseLeftButtonDown method to determines if mouse was clicked on the ScrollBar of the ScrollView and if so then just ignore the event and let the user use the Scroll. 

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;
}

Setting the initial offset and DragMode happens acctually in method ResolveOffset

/// <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;
}

the validation thet the clicked element is a chiled of the DragCanvas happens in the FindCanvasChild

/// <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;
}

And finally the btinging the element to front happens by the method BringToFront, and of course there is the oppossite method SendToBack, both of them using the same helping method 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
}

Now that we have our element checked and verified, brought to front and initial offsets registered, it the time to start moving it aroud

Stage 2: Calulating and Updating New Offset

the whole calculation magic accurs in a single overriden method OnPreviewMouseMove. There is not much to explain here it just some junior school math, but here are some differences between the original implementation of Dragging Elements in a Canvas by John Smith, one of them is the very noticible that there is some code duplication in the if statments and no method  CalculateDragElementRect this changes where made to prevent some calculation overhead.

in addition the initial mouse cursor location is updated each time and that is done for fixing the bug of when dragging out of view is disallowed, the bug comes to view when the element reached the DragCanvas bounds and the mouse keeps moving out of its bounderies there was a need to move the mouse some distance back before the element started moving again.

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
}

Stage 3: Finalizing the dragging

the intuitive for us as users is when the mouse button released the element is fixated and now the lement will stay in place even if the mouse keeps moving. to accomplish this simple task the next overriden method OnPreviewMouseUp does the task, actually it just nullifies the dragged element and set the flag m_IsDragInProgress to 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;
}

Possible Improvements

along side with the suggested in the original article Dragging Elements in a Canvas by John Smith, Perhaps, exposing some drag-related events might be helpful in some scenarios, such as a cancelable BeforeElementDrag event, an ElementDrag event which provides info about the drag element and its location, and an AfterElementDrag event. some issue exist and that is for example when the dragged element is a TextBox or some UserControl containg one of the derived classes form TextBoxBase then selecting a text in such element will not be possible.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here