Click here to Skip to main content
15,891,828 members
Articles / Desktop Programming / WPF

WPF Gadget Container Control

Rate me:
Please Sign up or sign in to vote.
4.00/5 (13 votes)
6 Mar 2009CPOL3 min read 74.1K   4.7K   51  
A Vista-styled gadget container control.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace GadgetLibrary
{
    /// <summary>
    /// This is a modified version of Josh Smith's DragCanvas (see http://www.codeproject.com/KB/WPF/DraggingElementsInCanvas.aspx)
    /// to support snap regions, allowing gadgets to snap into left, top, right and bottom regions
    /// A Canvas which manages dragging of the UIElements it contains.  
    /// </summary>
    public class SnapCanvas : Canvas
    {
        #region Data

        // Stores a reference to the UIElement currently being dragged by the user.
        private Rect _BottomSnapRect;
        private UIElement _ElementBeingDragged;
        private bool _IsDragInProgress;
        private Rect _LeftSnapRect;
        private bool _ModifyLeftOffset, _ModifyTopOffset;

        // Keeps track of where the mouse cursor was when a drag operation began.		
        private Point _OriginalCursorLocation;

        // The offsets from the SnapCanvas' edges when the drag operation began.
        private double _OriginalHorizontalOffset, _OriginalVerticalOffset;

        private Rect _RightSnapRect;
        private Rect _TopSnapRect;

        // Keeps track of which horizontal and vertical offset should be modified for the drag element.

        #endregion // Data

        #region Attached Properties

        #region CanBeDragged

        public static readonly DependencyProperty CanBeDraggedProperty;

        public static bool GetCanBeDragged(UIElement uiElement)
        {
            if (uiElement == null)
            {
                return false;
            }

            return (bool) uiElement.GetValue(CanBeDraggedProperty);
        }

        public static void SetCanBeDragged(UIElement uiElement, bool value)
        {
            if (uiElement != null)
            {
                uiElement.SetValue(CanBeDraggedProperty, value);
            }
        }

        #endregion // CanBeDragged

        #endregion // Attached Properties

        #region Dependency Properties

        public static readonly DependencyProperty AllowDraggingProperty;
        public static readonly DependencyProperty AllowDragOutOfViewProperty;

        public static readonly DependencyProperty SnapThresholdProperty = DependencyProperty.RegisterAttached(
            "SnapThreshold",
            typeof (double),
            typeof (SnapCanvas),
            new FrameworkPropertyMetadata(20d, FrameworkPropertyMetadataOptions.AffectsRender));

        public double SnapThreshold
        {
            get { return (double) GetValue(SnapThresholdProperty); }
            set { SetValue(SnapThresholdProperty, value); }
        }

        #endregion // Dependency Properties

        #region Static Constructor

        static SnapCanvas()
        {
            AllowDraggingProperty = DependencyProperty.Register(
                "AllowDragging",
                typeof (bool),
                typeof (SnapCanvas),
                new PropertyMetadata(true));

            AllowDragOutOfViewProperty = DependencyProperty.Register(
                "AllowDragOutOfView",
                typeof (bool),
                typeof (SnapCanvas),
                new UIPropertyMetadata(false));

            CanBeDraggedProperty = DependencyProperty.RegisterAttached(
                "CanBeDragged",
                typeof (bool),
                typeof (SnapCanvas),
                new UIPropertyMetadata(true));
        }

        #endregion // Static Constructor

        #region Constructor

        public SnapCanvas()
        {
            SizeChanged += SnapCanvas_SizeChanged;
        }

        #endregion // Constructor

        private void SnapCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            SetSnapRectanges(e.NewSize);
            RelocateSnappedContainers();
        }

        private void RelocateSnappedContainers()
        {
            foreach (object child in Children)
            {
                if (child is IGadgetContainer)
                {
                    var gadgetContainer = child as IGadgetContainer;

                    if (!gadgetContainer.SnapRegion.Equals(SnapRegions.NotSet))
                    {
                        SnapElement(gadgetContainer);
                    }
                }
            }
        }

        private void SetSnapRectanges(Size size)
        {
            _LeftSnapRect.Location = new Point(0, 0);
            _LeftSnapRect.Size = new Size(SnapThreshold, size.Height);

            _RightSnapRect.Location = new Point(size.Width - SnapThreshold, 0);
            _RightSnapRect.Size = new Size(SnapThreshold, size.Height);

            _TopSnapRect.Location = new Point(SnapThreshold, 0);
            _TopSnapRect.Size = new Size(size.Width - (SnapThreshold*2), SnapThreshold);

            _BottomSnapRect.Location = new Point(SnapThreshold, size.Height - SnapThreshold);
            _BottomSnapRect.Size = new Size(size.Width - (SnapThreshold*2), SnapThreshold);
        }

        #region Interface

        #region AllowDragging

        /// <summary>
        /// Gets/sets whether elements in the SnapCanvas should be draggable by the user.
        /// The default value is true.  This is a dependency property.
        /// </summary>
        public bool AllowDragging
        {
            get { return (bool) base.GetValue(AllowDraggingProperty); }
            set { base.SetValue(AllowDraggingProperty, value); }
        }

        #endregion // AllowDragging

        #region AllowDragOutOfView

        /// <summary>
        /// Gets/sets whether the user should be able to drag elements in the SnapCanvas out of
        /// the viewable area.  The default value is false.  This is a dependency property.
        /// </summary>
        public bool AllowDragOutOfView
        {
            get { return (bool) GetValue(AllowDragOutOfViewProperty); }
            set { SetValue(AllowDragOutOfViewProperty, value); }
        }

        #endregion // AllowDragOutOfView			

        #region BringToFront / SendToBack

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="element">
        /// The element to be sent to the front of the z-order.
        /// </param>
        public void BringToFront(UIElement element)
        {
            UpdateZOrder(element, true);
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="element">
        /// The element to be sent to the back of the z-order.
        /// </param>
        public void SendToBack(UIElement element)
        {
            UpdateZOrder(element, false);
        }

        #endregion // BringToFront / SendToBack

        #region ElementBeingDragged

        /// <summary>
        /// Returns the UIElement currently being dragged, or null.
        /// </summary>
        /// <remarks>
        /// Note to inheritors: This property exposes a protected 
        /// setter which should be used to modify the drag element.
        /// </remarks>
        public UIElement ElementBeingDragged
        {
            get
            {
                if (!AllowDragging)
                {
                    return null;
                }
                return _ElementBeingDragged;
            }
            protected set
            {
                if (_ElementBeingDragged != null)
                {
                    _ElementBeingDragged.ReleaseMouseCapture();
                }

                if (!AllowDragging)
                {
                    _ElementBeingDragged = null;
                }
                else
                {
                    if (GetCanBeDragged(value))
                    {
                        _ElementBeingDragged = value;
                        _ElementBeingDragged.CaptureMouse();
                    }
                    else
                    {
                        _ElementBeingDragged = null;
                    }
                }
            }
        }

        #endregion // ElementBeingDragged

        #region FindCanvasChild

        /// <summary>
        /// Walks up the visual tree starting with the specified DependencyObject, 
        /// 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.
        /// </summary>
        /// <param name="depObj">
        /// A DependencyObject from which the search begins.
        /// </param>
        public 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.
                var 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;
        }

        #endregion // FindCanvasChild

        #endregion // Interface

        #region Overrides

        #region OnPreviewMouseLeftButtonDown

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

            _IsDragInProgress = false;

            // Cache the mouse cursor location.
            _OriginalCursorLocation = e.GetPosition(this);

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

            // Get the element's offsets from the four sides of the Canvas.
            var left = GetLeft(ElementBeingDragged);
            var right = GetRight(ElementBeingDragged);
            var top = GetTop(ElementBeingDragged);
            var bottom = GetBottom(ElementBeingDragged);

            // Calculate the offset deltas and determine for which sides
            // of the Canvas to adjust the offsets.
            _OriginalHorizontalOffset = ResolveOffset(left, right, out _ModifyLeftOffset);
            _OriginalVerticalOffset = ResolveOffset(top, bottom, out _ModifyTopOffset);

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

            _IsDragInProgress = true;
        }

        #endregion // OnPreviewMouseLeftButtonDown

        #region OnPreviewMouseMove

        protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            base.OnPreviewMouseMove(e);

            // If no element is being dragged, there is nothing to do.
            if (ElementBeingDragged == null || !_IsDragInProgress)
            {
                return;
            }

            // Get the position of the mouse cursor, relative to the Canvas.
            var cursorLocation = e.GetPosition(this);

            // These values will store the new offsets of the drag element.
            double newHorizontalOffset, newVerticalOffset;

            #region Calculate Offsets

            // Determine the horizontal offset.
            if (_ModifyLeftOffset)
            {
                newHorizontalOffset = _OriginalHorizontalOffset + (cursorLocation.X - _OriginalCursorLocation.X);
            }
            else
            {
                newHorizontalOffset = _OriginalHorizontalOffset - (cursorLocation.X - _OriginalCursorLocation.X);
            }

            // Determine the vertical offset.
            if (_ModifyTopOffset)
            {
                newVerticalOffset = _OriginalVerticalOffset + (cursorLocation.Y - _OriginalCursorLocation.Y);
            }
            else
            {
                newVerticalOffset = _OriginalVerticalOffset - (cursorLocation.Y - _OriginalCursorLocation.Y);
            }

            #endregion // Calculate Offsets

            if (! AllowDragOutOfView)
            {
                #region Verify Drag Element Location

                // Get the bounding rect of the drag element.
                var elemRect = CalculateDragElementRect(newHorizontalOffset, newVerticalOffset);

                //
                // If the element is being dragged out of the viewable area, 
                // determine the ideal rect location, so that the element is 
                // within the edge(s) of the canvas.
                //
                var leftAlign = elemRect.Left < 0;
                var rightAlign = elemRect.Right > ActualWidth;

                if (leftAlign)
                {
                    newHorizontalOffset = _ModifyLeftOffset ? 0 : ActualWidth - elemRect.Width;
                }
                else if (rightAlign)
                {
                    newHorizontalOffset = _ModifyLeftOffset ? ActualWidth - elemRect.Width : 0;
                }

                var topAlign = elemRect.Top < 0;
                var bottomAlign = elemRect.Bottom > ActualHeight;

                if (topAlign)
                {
                    newVerticalOffset = _ModifyTopOffset ? 0 : ActualHeight - elemRect.Height;
                }
                else if (bottomAlign)
                {
                    newVerticalOffset = _ModifyTopOffset ? ActualHeight - elemRect.Height : 0;
                }

                #endregion // Verify Drag Element Location
            }

            #region Move Drag Element

            if (_ModifyLeftOffset)
            {
                SetLeft(ElementBeingDragged, newHorizontalOffset);
            }
            else
            {
                SetRight(ElementBeingDragged, newHorizontalOffset);
            }

            if (_ModifyTopOffset)
            {
                SetTop(ElementBeingDragged, newVerticalOffset);
            }
            else
            {
                SetBottom(ElementBeingDragged, newVerticalOffset);
            }

            #endregion // Move Drag Element

            if (ElementBeingDragged is IGadgetContainer)
            {
                var gadgetContainer = ElementBeingDragged as IGadgetContainer;

                //container is being dragged, so "unsnap" it
                gadgetContainer.SnapRegion = SnapRegions.NotSet;
                SnapElement(gadgetContainer);
            }
        }

        #endregion // OnPreviewMouseMove

        private void SnapElement(IGadgetContainer gadgetContainer)
        {
            var element = gadgetContainer as UIElement;

            if (element != null)
            {
                var elementRect = new Rect
                                      {
                                          X = GetLeft(element),
                                          Y = GetTop(element),
                                          Size = element.RenderSize
                                      };

                //here we need to find out if the dragged element resides in one of the snap rectangles
                if (_LeftSnapRect.IntersectsWith(elementRect) || gadgetContainer.SnapRegion.Equals(SnapRegions.Left))
                {
                    SetLeft(element, 0);
                    gadgetContainer.SnapRegion = SnapRegions.Left;
                    return;
                }

                if (_RightSnapRect.IntersectsWith(elementRect) || gadgetContainer.SnapRegion.Equals(SnapRegions.Right))
                {
                    SetLeft(element, ActualWidth - elementRect.Width);
                    gadgetContainer.SnapRegion = SnapRegions.Right;
                    return;
                }

                if (_TopSnapRect.IntersectsWith(elementRect) || gadgetContainer.SnapRegion.Equals(SnapRegions.Top))
                {
                    SetTop(element, 0);
                    gadgetContainer.SnapRegion = SnapRegions.Top;
                    return;
                }

                if (_BottomSnapRect.IntersectsWith(elementRect) || gadgetContainer.SnapRegion.Equals(SnapRegions.Bottom))
                {
                    SetTop(element, ActualHeight - elementRect.Height);
                    gadgetContainer.SnapRegion = SnapRegions.Bottom;
                }
            }
        }

        #region OnHostPreviewMouseUp

        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.
            ElementBeingDragged = null;
        }

        #endregion // OnHostPreviewMouseUp

        #endregion // Host Event Handlers

        #region Private Helpers

        #region CalculateDragElementRect

        /// <summary>
        /// Returns a Rect which describes the bounds of the element being dragged.
        /// </summary>
        private Rect CalculateDragElementRect(double newHorizOffset, double newVertOffset)
        {
            if (ElementBeingDragged == null)
            {
                throw new InvalidOperationException("ElementBeingDragged is null.");
            }

            var elemSize = ElementBeingDragged.RenderSize;

            double x, y;

            if (_ModifyLeftOffset)
            {
                x = newHorizOffset;
            }
            else
            {
                x = ActualWidth - newHorizOffset - elemSize.Width;
            }

            if (_ModifyTopOffset)
            {
                y = newVertOffset;
            }
            else
            {
                y = ActualHeight - newVertOffset - elemSize.Height;
            }

            var elemLoc = new Point(x, y);

            return new Rect(elemLoc, elemSize);
        }

        #endregion // CalculateDragElementRect

        #region ResolveOffset

        /// <summary>
        /// Determines one component of a UIElement's location 
        /// within a Canvas (either the horizontal or vertical offset).
        /// </summary>
        /// <param name="side1">
        /// The value of an offset relative to a default side of the 
        /// Canvas (i.e. top or left).
        /// </param>
        /// <param name="side2">
        /// The value of the offset relative to the other side of the 
        /// Canvas (i.e. bottom or right).
        /// </param>
        /// <param name="useSide1">
        /// Will be set to true if the returned value should be used 
        /// for the offset from the side represented by the 'side1' 
        /// parameter.  Otherwise, it will be set to false.
        /// </param>
        private static double ResolveOffset(double side1, double side2, out bool useSide1)
        {
            // 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.
            useSide1 = true;
            double result;
            if (Double.IsNaN(side1))
            {
                if (Double.IsNaN(side2))
                {
                    // Both sides have no value, so set the
                    // first side to a value of zero.
                    result = 0;
                }
                else
                {
                    result = side2;
                    useSide1 = false;
                }
            }
            else
            {
                result = side1;
            }
            return result;
        }

        #endregion // ResolveOffset

        #region UpdateZOrder

        /// <summary>
        /// Helper method used by the BringToFront and SendToBack methods.
        /// </summary>
        /// <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-Indici 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 = GetZIndex(element);

            #endregion // Calculate Z-Indici And Offset

            #region Update Z-Indici

            // Update the Z-Index of every UIElement in the Canvas.
            foreach (UIElement childElement in base.Children)
            {
                if (childElement == element)
                {
                    SetZIndex(element, elementNewZIndex);
                }
                else
                {
                    int zIndex = 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)
                    {
                        SetZIndex(childElement, zIndex + offset);
                    }
                }
            }

            #endregion // Update Z-Indici
        }

        #endregion // UpdateZOrder

        #endregion // Private Helpers
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I'm a software developer living in Northampton, UK, and enjoy working with the latest .Net technologies.

Comments and Discussions