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)

About these ads

2 thoughts on “WPF. Draggable objects and simple shape connectors.

  1. Cool, thanks…

    I will defiantly use Thumb object on my next project. Until now I worked with a DragDrop manager which created an Adorner.
    It is good on some cases, but on some others, it seems that Thumb rulea

  2. very useful …

    it helped me a lot..
    thanks a lot for such a nice article.

    how can we make it possible that thumbs should not overlap on each other while we drag them around the canvas(or say any contain holder)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s