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
:
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.
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 ); m_IsDragInProgress = false;
m_InitialCursorLocation = e.GetPosition(this);
if (null != GetParentOfType<scrollbar>(e.OriginalSource as DependencyObject)) { return; }
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));
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
private ModificationDirection ResolveOffset(double Left, double Right, double Top, double Bottom)
{
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
private UIElement FindCanvasChild(DependencyObject depObj)
{
while (depObj != null)
{
UIElement elem = depObj as UIElement;
if (elem != null && base.Children.Contains(elem))
{
break;
}
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
public void BringToFront(UIElement element)
{
UpdateZOrder(element, true);
}
public void SendToBack(UIElement element)
{
UpdateZOrder(element, fasle);
}
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
#region Calculate Z-Indexes And Offset
int elementNewZIndex = -1;
if (bringToFront)
{
foreach (UIElement elem in base.Children)
{
if (elem.Visibility != Visibility.Collapsed)
{
++elementNewZIndex;
}
}
}
else
{
elementNewZIndex = 0;
}
int offset = (elementNewZIndex == 0) ? +1 : -1;
int elementCurrentZIndex = Canvas.GetZIndex(element);
#endregion
#region Update Z-Indexes
foreach (UIElement childElement in base.Children)
{
if (childElement == element)
{
Canvas.SetZIndex(element, elementNewZIndex);
}
else
{
int zIndex = Canvas.GetZIndex(childElement);
if (bringToFront && elementCurrentZIndex < zIndex || !bringToFront && zIndex < elementCurrentZIndex)
{
Canvas.SetZIndex(childElement, zIndex + offset);
}
}
}
#endregion }
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 (m_DraggedElement == null || !m_IsDragInProgress)
{
return;
}
Point cursorLocation = e.GetPosition(this);
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
#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 }
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);
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.