RSS

Category Archives: Computer and Internet

WPF. Draggable objects and simple shape connectors.


Last weekend I found very good sample of using Thumb class for implementing draggable objects. In two words as all other elements in WPF the Thumb also can be templated. And nothing stops you to get all drag functionality of Thumb turning it to anything you need. That’s perfect I think ;)

So how to easily create a simple draggable object based on a Thumb?

<Thumb Name="myThumb" DragDelta="onDragDelta" Canvas.Left="0" Canvas.Top="0" Template="{StaticResource template1}"/>

Here we declare a Thumb, set the “onDragDelta” handler for the “DragDelta” event and assign to it custom template called “template1″.

After that we create the most simpliest shape template that can be found everywhere in the internet

<ControlTemplate x:Key="template1">
<Ellipse Width="60" Height="30" Fill="Black"/>
</ControlTemplate>

As you can see it turns our thumb to a dummy black shape of (60;30) size.

The complete xaml for the window will be as following

<Window x:Class="ShapeConnectors.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="DragableObject" Height="300" Width="300"
    >
<Window.Resources>
<ResourceDictionary>
<ControlTemplate x:Key="template1">
<Ellipse Width="60" Height="30" Fill="Black"/>
</ControlTemplate>
</ResourceDictionary>
</Window.Resources>

<Canvas Name="myCanvas">
<Thumb Name="myThumb" DragDelta="onDragDelta" Canvas.Left="0" Canvas.Top="0" Template="{StaticResource template1}"/>
</Canvas>
</Window>

draggableobject

As our dummy black shape is still a Thumb element it needs actually one event hanler for the basic drag support implementation – “onDragDelta”. Implementing code behind is this way is too boring…

void onDragDelta(object sender, DragDeltaEventArgs e)
        {
            Canvas.SetLeft(myThumb, Canvas.GetLeft(myThumb) + e.HorizontalChange);
            Canvas.SetTop(myThumb, Canvas.GetTop(myThumb) + e.VerticalChange);
        }

We get the original position of the element being dragged and add the new offset values.

After playing a couple of minutes with the sample above I decided to complicate it a bit to get something more intresting. I wanted to create some workflow-like draggable objects connected to each other with simple shape connectors using basic line geometry. Upon moving the shapes across the canvas line connectors should followed the objects too. Additionally I wanted to have possibility of adding new shapes by clicking at any place of the canvas with establishing any simple line connector to the existing object.

Something like this

shapeconnectors_1 shapeconnectors_2 shapeconnectors_3

As far as we get the task to play, what will be the most simple concept of getting the desired result?

Each shape can possibly be connected to any number of another shapes. For hanling the position of each connector while dragging the object we need to somehow control the start and end points of the line element connected to two shapes. So it becomes obvious that each shape should contain the list of line’s start and end points separately so that line positioning and length can be easily updated by the shape itself or outter code.

Let’s inherit the basic Thumb class providing the required functionality

MyThumb.cs

using System.Collections.Generic;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace ShapeConnectors
{
    public class MyThumb : Thumb
    {
        public List<LineGeometry> EndLines { get; private set; }
        public List<LineGeometry> StartLines { get; private set; }

        public MyThumb() : base() 
        {
            StartLines = new List<LineGeometry>();
            EndLines = new List<LineGeometry>();
        }
    }
}

It’s very easy now to change the xaml part to use our extended Thumb element

<Window x:Class="ShapeConnectors.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:ShapeConnectors"
    Title="Window1" Height="376.25" Width="801.25" Loaded="Window_Loaded">
<Canvas Name="myCanvas">
<my:MyThumb x:Name="myThumb1" DragDelta="onDragDelta" Canvas.Left="270" Canvas.Top="63.75" Template="{StaticResource template1}"/>
</Canvas>
</Window>

Here we define our hamespace “ShapeConnectors” and prefix “my”.
Note that should name the element by using “x:Name” syntax because we put the existing though inherited element to xaml.
As can be seen from the screenshots, our shape should contain an icon and a name elements. Let’s change thumb’s template to get it working.

<ControlTemplate x:Key="template1">
<StackPanel>
<Image Name="tplImage" Source="Images/user1.png" Stretch="Uniform" Width="32" Height="32"/>
<TextBlock Name="tplTextBlock" Text="User stage"/>
</StackPanel>
</ControlTemplate>

We provide a default template for all the draggable objects. Each object contains an image element referencing “Images/user1.png” picture from the resources and contains a text block “User stage” (you can change it to anything you want). Later we will access this template directly from the code, so it is important to name the elements.

Full xaml for our window will be the following

<Window x:Class="ShapeConnectors.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:ShapeConnectors"
    Title="Window1" Height="376" Width="801" Loaded="Window_Loaded">
<Window.Resources>
<ResourceDictionary>
<ControlTemplate x:Key="template1">
<StackPanel>
<Image Name="tplImage" Source="Images/user1.png" Stretch="Uniform" Width="32" Height="32"/>
<TextBlock Name="tplTextBlock" Text="User stage"/>
</StackPanel>
</ControlTemplate>
</ResourceDictionary>
</Window.Resources>
<Canvas Name="myCanvas">
<my:MyThumb x:Name="myThumb1" DragDelta="onDragDelta" Canvas.Left="270" Canvas.Top="63.75" Template="{StaticResource template1}"/>
<my:MyThumb x:Name="myThumb2" DragDelta="onDragDelta" Canvas.Left="270" Canvas.Top="212.5" Template="{StaticResource template1}"/>
<my:MyThumb x:Name="myThumb3" DragDelta="onDragDelta" Canvas.Left="430" Canvas.Top="212.5" Template="{StaticResource template1}"/>
<my:MyThumb x:Name="myThumb4" DragDelta="onDragDelta" Canvas.Left="430" Canvas.Top="63.75" Template="{StaticResource template1}"/>
<Button Canvas.Left="15" Canvas.Top="16" Height="22" Name="btnNewAction" Width="75" Click="btnNewAction_Click">new action</Button>
</Canvas>
</Window>

I’ve added four thumbs by default. Additionally I’ve created a button called “btnNewAction” that will be enabling the mode of adding new objects by clicking somewhere on the canvas. One button click – one thumb to be created anywhere on the canvas and linked to the predefined “myThumb2″ element.

As for line geometry we’ll be using the Path element. Each path element will be hosting one line.

So here’s going the main part of our application

Window1.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace ShapeConnectors
{
    public partial class Window1 : Window
    {
        // simple flag for enabling "New thumb" mode
        bool isAddNew = false;

        // Paths for our predefined thumbs
        Path path1;
        Path path2;
        Path path3;
        Path path4;

        public Window1()
        {
            InitializeComponent();
        }

        // Event hanlder for dragging functionality support same to all thumbs
        private void onDragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
        {            
            MyThumb thumb = e.Source as MyThumb;

            double left = Canvas.GetLeft(thumb) + e.HorizontalChange;
            double top = Canvas.GetTop(thumb) + e.VerticalChange;

            Canvas.SetLeft(thumb, left);
            Canvas.SetTop(thumb, top);

            // Update lines's layouts
            UpdateLines(thumb);            
        }

        // This method updates all the starting and ending lines assigned for the given thumb 
        // according to the latest known thumb position on the canvas
        private void UpdateLines(MyThumb thumb)
        {
            double left = Canvas.GetLeft(thumb);
            double top = Canvas.GetTop(thumb);

            for (int i = 0; i < thumb.StartLines.Count; i++)
                thumb.StartLines[i].StartPoint = new Point(left + thumb.ActualWidth / 2, top + thumb.ActualHeight / 2);

            for (int i = 0; i < thumb.EndLines.Count; i++)
                thumb.EndLines[i].EndPoint = new Point(left + thumb.ActualWidth / 2, top + thumb.ActualHeight / 2);
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Move all the predefined thumbs to the front to be over the lines
            Canvas.SetZIndex(myThumb1, 1);
            Canvas.SetZIndex(myThumb2, 1);
            Canvas.SetZIndex(myThumb3, 1);
            Canvas.SetZIndex(myThumb4, 1);

            #region Initialize paths for predefined thumbs
            path1 = new Path();
            path1.Stroke = Brushes.Black;
            path1.StrokeThickness = 1;

            path2 = new Path();
            path2.Stroke = Brushes.Blue;
            path2.StrokeThickness = 1;

            path3 = new Path();
            path3.Stroke = Brushes.Green;
            path3.StrokeThickness = 1;

            path4 = new Path();
            path4.Stroke = Brushes.Red;
            path4.StrokeThickness = 1;

            myCanvas.Children.Add(path1);
            myCanvas.Children.Add(path2);
            myCanvas.Children.Add(path3);
            myCanvas.Children.Add(path4); 
            #endregion

            #region Initialize line geometry for predefined thumbs
            LineGeometry line1 = new LineGeometry();
            path1.Data = line1;

            LineGeometry line2 = new LineGeometry();
            path2.Data = line2;

            LineGeometry line3 = new LineGeometry();
            path3.Data = line3;

            LineGeometry line4 = new LineGeometry();
            path4.Data = line4;
            #endregion

            #region Setup connections for predefined thumbs
            myThumb1.StartLines.Add(line1);
            myThumb2.EndLines.Add(line1);

            myThumb2.StartLines.Add(line2);
            myThumb3.EndLines.Add(line2);

            myThumb3.StartLines.Add(line3);
            myThumb4.EndLines.Add(line3);

            myThumb4.StartLines.Add(line4);
            myThumb1.EndLines.Add(line4); 
            #endregion

            #region Update lines' layouts
            UpdateLines(myThumb1);
            UpdateLines(myThumb2);
            UpdateLines(myThumb3);
            UpdateLines(myThumb4); 
            #endregion
            
            this.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(Window1_PreviewMouseLeftButtonDown);
        }

        // Event handler for creating new thumb element by left mouse click
        // and visually connecting it to the myThumb2 element
        void Window1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (isAddNew)
            {
                // Create new thumb object
                MyThumb newThumb = new MyThumb();   
                // Assign our custom template to it
                newThumb.Template = this.Resources["template1"] as ControlTemplate;
                // Calling ApplyTemplate enables us to navigate the visual tree right now (important!)
                newThumb.ApplyTemplate();
                // Add the "onDragDelta" event handler that is common to all objects
                newThumb.DragDelta +=new DragDeltaEventHandler(onDragDelta);
                // Put newly created thumb on the canvas
                myCanvas.Children.Add(newThumb);

                // Access the image element of our custom template and assign it to the new image
                Image img = (Image)newThumb.Template.FindName("tplImage", newThumb);
                img.Source = new BitmapImage(new Uri("Images/gear_connection.png", UriKind.Relative));

                // Access the textblock element of template and change it too
                TextBlock txt = (TextBlock)newThumb.Template.FindName("tplTextBlock", newThumb);
                txt.Text = "System action";

                // Set the position of the object according to the mouse pointer                
                Point position = e.GetPosition(this);
                Canvas.SetLeft(newThumb, position.X);
                Canvas.SetTop(newThumb, position.Y);
                // Move our thumb to the front to be over the lines
                Canvas.SetZIndex(newThumb, 1);
                // Manually update the layout of the thumb (important!)
                newThumb.UpdateLayout();

                // Create new path and put it on the canvas
                Path newPath = new Path();
                newPath.Stroke = Brushes.Black;
                newPath.StrokeThickness = 1;
                myCanvas.Children.Add(newPath);

                // Create new line geometry element and assign the path to it
                LineGeometry newLine = new LineGeometry();
                newPath.Data = newLine;

                // Predefined "myThumb2" element will host the starting point
                myThumb2.StartLines.Add(newLine);
                // Our new thumb will host the ending point of the line
                newThumb.EndLines.Add(newLine);

                // Update the layout of line geometry
                UpdateLines(newThumb);
                UpdateLines(myThumb2);

                isAddNew = false;                
                Mouse.OverrideCursor = null;
                btnNewAction.IsEnabled = true;
                e.Handled = true;
            }
        }

        // Event handler for enabling new thumb creation by left mouse button click
        private void btnNewAction_Click(object sender, RoutedEventArgs e)
        {
            isAddNew = true;
            Mouse.OverrideCursor = Cursors.SizeAll;
            btnNewAction.IsEnabled = false;
        }
    }
}

Here’s what we can have upon playing a bit with the applicaition

shapeconnectors_4

This sample if too far from the real life application but I tried to keep the code as simple as possible for all to be able to investigate the process and find out own ways of implementing the desired idea.

Have a nice testing and coding.

Source code for the article (VS 2012)

 
2 Comments

Posted by on October 13, 2007 in Computer and Internet

 

The WPF Resizing Adorner for Canvas


MSDN library for Visual Studio 2008 beta 2 gives three samples of using adorners: "Simple Circle Adorner", "Add and Remove Adorners" and "Resizing Adorner". All the samples were designed to work with the Grid control so the behavior is not very good when we move to the Canvas or some other panel.

Here’s the code for the Canvas usage. No optimizations and other professional stuff, just the pure idea.

Red lines are exactly the quick patch for the original sample

ResizingAdorner.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace adorners
{
    public class ResizingAdorner : Adorner
    {
        // Resizing adorner uses Thumbs for visual elements. 
        // The Thumbs have built-in mouse input handling.
        Thumb topLeft, topRight, bottomLeft, bottomRight;

        // To store and manage the adorner’s visual children.
        VisualCollection visualChildren;

        // Initialize the ResizingAdorner.
        public ResizingAdorner(UIElement adornedElement)
            : base(adornedElement)
        {           
            visualChildren = new VisualCollection(this);

            // Call a helper method to initialize the Thumbs
            // with a customized cursors.
            BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
            BuildAdornerCorner(ref topRight, Cursors.SizeNESW);
            BuildAdornerCorner(ref bottomLeft, Cursors.SizeNESW);
            BuildAdornerCorner(ref bottomRight, Cursors.SizeNWSE);

            // Add handlers for resizing.
            bottomLeft.DragDelta += new DragDeltaEventHandler(HandleBottomLeft);
            bottomRight.DragDelta += new DragDeltaEventHandler(HandleBottomRight);
            topLeft.DragDelta += new DragDeltaEventHandler(HandleTopLeft);
            topRight.DragDelta += new DragDeltaEventHandler(HandleTopRight);
        }

        // Handler for resizing from the bottom-right.
        void HandleBottomRight(object sender, DragDeltaEventArgs args)
        {
            FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
            Thumb hitThumb = sender as Thumb;

            if (adornedElement == null || hitThumb == null) return;
            FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;

            // Ensure that the Width and Height are properly initialized after the resize.
            EnforceSize(adornedElement);

            // Change the size by the amount the user drags the mouse, as long as it’s larger
            // than the width or height of an adorner, respectively.
            adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
            adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);
        }

        // Handler for resizing from the top-right.
        void HandleTopRight(object sender, DragDeltaEventArgs args)
        {
            FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
            Thumb hitThumb = sender as Thumb;

            if (adornedElement == null || hitThumb == null) return;
            FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;

            // Ensure that the Width and Height are properly initialized after the resize.
            EnforceSize(adornedElement);

            // Change the size by the amount the user drags the mouse, as long as it’s larger
            // than the width or height of an adorner, respectively.
            adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
            //adornedElement.Height = Math.Max(adornedElement.Height – args.VerticalChange, hitThumb.DesiredSize.Height);

            double height_old = adornedElement.Height;
            double height_new = Math.Max(adornedElement.Height – args.VerticalChange, hitThumb.DesiredSize.Height);
            double top_old = Canvas.GetTop(adornedElement);
            adornedElement.Height = height_new;
            Canvas.SetTop(adornedElement, top_old – (height_new – height_old));

        }

        // Handler for resizing from the top-left.
        void HandleTopLeft(object sender, DragDeltaEventArgs args)
        {
            FrameworkElement adornedElement = AdornedElement as FrameworkElement;
            Thumb hitThumb = sender as Thumb;

            if (adornedElement == null || hitThumb == null) return;

            // Ensure that the Width and Height are properly initialized after the resize.
            EnforceSize(adornedElement);

            // Change the size by the amount the user drags the mouse, as long as it’s larger
            // than the width or height of an adorner, respectively.

            //adornedElement.Width = Math.Max(adornedElement.Width – args.HorizontalChange, hitThumb.DesiredSize.Width);
            //adornedElement.Height = Math.Max(adornedElement.Height – args.VerticalChange, hitThumb.DesiredSize.Height
);

            double width_old = adornedElement.Width;
            double width_new = Math.Max(adornedElement.Width – args.HorizontalChange, hitThumb.DesiredSize.Width);
            double left_old = Canvas.GetLeft(adornedElement);
            adornedElement.Width = width_new;
            Canvas.SetLeft(adornedElement, left_old – (width_new – width_old));
            double height_old = adornedElement.Height;
            double height_new = Math.Max(adornedElement.Height – args.VerticalChange, hitThumb.DesiredSize.Height);
            double top_old = Canvas.GetTop(adornedElement);
            adornedElement.Height = height_new;
            Canvas.SetTop(adornedElement, top_old – (height_new – height_old));
        }

        // Handler for resizing from the bottom-left.
        void HandleBottomLeft(object sender, DragDeltaEventArgs args)
        {
            FrameworkElement adornedElement = AdornedElement as FrameworkElement;
            Thumb hitThumb = sender as Thumb;

            if (adornedElement == null || hitThumb == null) return;

            // Ensure that the Width and Height are properly initialized after the resize.
            EnforceSize(adornedElement);

            // Change the size by the amount the user drags the mouse, as long as it’s larger
            // than the width or height of an adorner, respectively.
            //adornedElement.Width = Math.Max(adornedElement.Width – args.HorizontalChange, hitThumb.DesiredSize.Width);
            adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);

            double width_old = adornedElement.Width;
            double width_new = Math.Max(adornedElement.Width – args.HorizontalChange, hitThumb.DesiredSize.Width);
            double left_old = Canvas.GetLeft(adornedElement);
            adornedElement.Width = width_new;           
            Canvas.SetLeft(adornedElement, left_old – (width_new – width_old));

        }

        // Arrange the Adorners.
        protected override Size ArrangeOverride(Size finalSize)
        {
            // desiredWidth and desiredHeight are the width and height of the element that’s being adorned. 
            // These will be used to place the ResizingAdorner at the corners of the adorned element.
 
            double desiredWidth = AdornedElement.DesiredSize.Width;
            double desiredHeight = AdornedElement.DesiredSize.Height;
            // adornerWidth & adornerHeight are used for placement as well.
            double adornerWidth = this.DesiredSize.Width;
            double adornerHeight = this.DesiredSize.Height;

            topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
            topRight.Arrange(new Rect(desiredWidth – adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
            bottomLeft.Arrange(new Rect(-adornerWidth / 2, desiredHeight – adornerHeight / 2, adornerWidth, adornerHeight));
            bottomRight.Arrange(new Rect(desiredWidth – adornerWidth / 2, desiredHeight – adornerHeight / 2, adornerWidth, adornerHeight));

            // Return the final size.
            return finalSize;
        }

        // Helper method to instantiate the corner Thumbs, set the Cursor property,
        // set some appearance properties, and add the elements to the visual tree.

        void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor)
        {
            if (cornerThumb != null) return;

            cornerThumb = new Thumb();

            // Set some arbitrary visual characteristics.
            cornerThumb.Cursor = customizedCursor;
            cornerThumb.Height = cornerThumb.Width = 10;
            cornerThumb.Opacity = 0.40;
            cornerThumb.Background = new SolidColorBrush(Colors.MediumBlue);

            visualChildren.Add(cornerThumb);
        }

        // This method ensures that the Widths and Heights are initialized.  Sizing to content produces
        // Width and Height values of Double.NaN.  Because this Adorner explicitly resizes, the Width and Height
        // need to be set first.  It also sets the maximum size of the adorned element.

        void EnforceSize(FrameworkElement adornedElement)
        {
            if (adornedElement.Width.Equals(Double.NaN))
                adornedElement.Width = adornedElement.DesiredSize.Width;
            if (adornedElement.Height.Equals(Double.NaN))
                adornedElement.Height = adornedElement.DesiredSize.Height;

            FrameworkElement parent = adornedElement.Parent as FrameworkElement;
            if (parent != null)
            {
                adornedElement.MaxHeight = parent.ActualHeight;
                adornedElement.MaxWidth = parent.ActualWidth;
            }
        }
        // Override the VisualChildrenCount and GetVisualChild properties to interface with
        // the adorner’s visual collection.
        protected override int VisualChildrenCount { get { return visualChildren.Count; } }
        protected override Visual GetVisualChild(int index) { return visualChildren[index]; }
    }
}

 

That’s all. Now resizing adorner works as it is supposed to work. The element is resized correctly.

Have a nice day.

 
6 Comments

Posted by on October 13, 2007 in Computer and Internet

 
 
Follow

Get every new post delivered to your Inbox.

Join 71 other followers

%d bloggers like this: