5,138,728 members and growing! (16,306 online)
Email Password   helpLost your password?
Platforms, Frameworks & Libraries » Windows Presentation Foundation » General     Intermediate License: The Code Project Open License (CPOL)

WPF Diagram Designer: Part 1

By sukram

Drag, resize and rotate elements on a Canvas
XML, C# 3.0, C#, Windows, .NET, .NET 3.5, WPF, Dev

Posted: 16 Jan 2008
Updated: 7 Feb 2008
Views: 17,913
Announcements



Search    
Advanced Search
Sitemap
29 votes for this Article.
Popularity: 6.97 Rating: 4.77 out of 5
0 votes, 0.0%
1
0 votes, 0.0%
2
1 vote, 3.4%
3
1 vote, 3.4%
4
27 votes, 93.1%
5

Introduction

In this first article of the series, I will show you how to drag, resize and rotate any content on a Canvas. With "any content," I mean any object you can assign to the Content property of a ContentControl, and since that property is of type object, you can set it to any object. The only exception I know of is that you cannot set the Content property to an object of type Window, because in WPF a Window must be the root of a visual tree.

Static Designer Item

Let's start with a simple diagram:

<Canvas>
  <Ellipse Fill="Blue" Width="130" Height="130" Canvas.Top="100" Canvas.Left="100"/>
</Canvas>

This may not be very useful in terms of a designer application, but it is still a good starting point. We use a Canvas as a designer panel on which we position an Ellipse. As a first modification, we wrap the Ellipse into a ContentControl class:

<ContentControl Width="130" Height="130" Canvas.Top="100" Canvas.Left="100">
   <Ellipse Fill="Blue"/>
</ContentControl>

Next we create a template for the ContentControl so that we can define the visual appearance of the container.

<ControlTemplate x:Key="StaticDesignerItem" TargetType="ContentControl">
     <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</ControlTemplate>

<ContentControl Name="DesignerItem" Width="130" Height="130" 
                Canvas.Top="100" Canvas.Left="100"
                Template="{StaticResource StaticDesignerItem}">
   <Ellipse Fill="Blue"/>
</ContentControl>

This template doesn't bring us any benefit at the moment, but in the next chapters we will modify the template such that we can drag, resize and rotate any content on the canvas. That is important to grasp: all we need to drag, resize and rotate items on a canvas will be located in the template! By the way, from now on, the Name property of the ContentControl is set to DesignerItem. So if I write DesignerItem, I mean the ContentControl.

Draggable Designer Item

There exists a control in WPF about which the MSDN documentation says that it, " ...represents a control that lets the user drag and resize controls." That seems to be the perfect candidate for our job. Its name is Thumb and here is how we are going to use it:

 using System;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace DragAndResizeControl
{
    public class DragThumb : Thumb
    {
        public DragThumb()
        {
            base.DragDelta += new DragDeltaEventHandler(DragThumb_DragDelta);
        }

        void DragThumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            ContentControl item = this.DataContext as ContentControl;

            if (item != null)
            {
                double left = Canvas.GetLeft(item);
                double top = Canvas.GetTop(item);
                Canvas.SetLeft(item, left + e.HorizontalChange);
                Canvas.SetTop(item, top + e.VerticalChange);
            }
        }
    }
}

The DragThumb class is inherited from Thumb and all it provides is an event handler implementation. This implementation expects the DataContext property to be set to an object of type ContentControl, which actually is our DesignerItem. You will see later where this value comes from. Once we have the DesignerItem, we just update its position relative to the designer canvas. Quite simple, isn't it? So let's create a new template with DragThumb:

<ControlTemplate x:Key="DragableDesignerItem" TargetType="ContentControl">
   <Grid>
     <s:DragThumb DataContext=
             "{Binding RelativeSource= {RelativeSource TemplatedParent}}"/>
     <ContentPresenter Content="{TemplateBinding
             ContentControl.Content}"/>
   </Grid>
 </ControlTemplate>

 <Canvas>
   <ContentControl Name="DesignerItem" 
                   Template="{StaticResource DragableDesignerItem}"
                   Width="130" Height="130" Canvas.Top="100" Canvas.Left="100">
     <Ellipse Fill="Blue" />
   </ContentControl>
 </Canvas> 

Now you see where the Context property of DragThumb is set. It is bound to the templated parent, which of course is the DesignerItem. If you run this code, you will see a gray rectangle behind our ellipse. Where does it come from?

Default visual representation of a Thumb control in WPF

What you see behind the ellipses is the default ControlTemplate of the Thumb control. Our DragThumb control has inherited that template and that's why we see this gray rectangle. However, we can easily change that by defining our own template for the DragThumb class. Here we use a transparent Rectangle for the visual representation. But take care: you cannot drag the DragThumb where it is covered by the content of the ContentControl!

  <ControlTemplate x:Key="DragThumbTemplate" TargetType="{x:Type s:DragThumb}">
   <Rectangle Fill="Transparent"/>
 </ControlTemplate>

 <ControlTemplate x:Key="DragableDesignerItem" TargetType="ContentControl">
   <Grid>
     <s:DragThumb Template="{StaticResource DragThumbTemplate}" Cursor="SizeAll"
                  DataContext=
                  "{Binding RelativeSource= {RelativeSource TemplatedParent}}"/>
     <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
   </Grid>
 </ControlTemplate>

 <Canvas>
   <ContentControl Name="DesignerItem" Template="{StaticResource DragableDesignerItem}"
                   Width="130" Height="130" Canvas.Top="100" Canvas.Left="100">
     <Ellipse Fill="Blue" />
   </ContentControl>
 </Canvas>

Attention: We can start dragging the item only in that area of our DragThumb where it is not covered by the content of the DesignerItem! As a workaround for the moment, we set the IsHitTestVisible property of the Ellipse to false.

 <Ellipse Fill="Blue" IsHitTestVisible="False"/>

In the coming article, I will provide you with a more elegant solution for this issue. Now we take the next step, "What does it take to resize the DesignerItem?"

Resizable Designer Item

You remember that the MSDN documentation promised that the Thumb control would let the user drag and resize controls? So, we stick with the Thumb class and try the following:

 <ControlTemplate x:Key="ResizeDecoratorTemplate" TargetType="Control">
  <Grid>
    <Thumb Height="3" Cursor="SizeNS" Margin="0 -4 0 0"
           VerticalAlignment="Top" HorizontalAlignment="Stretch"/>
    <Thumb Width="3" Cursor="SizeWE" Margin="-4 0 0 0"
           VerticalAlignment="Stretch" HorizontalAlignment="Left"/>
    <Thumb Width="3" Cursor="SizeWE" Margin="0 0 -4 0"
           VerticalAlignment="Stretch" HorizontalAlignment="Right"/>
    <Thumb Height="3" Cursor="SizeNS" Margin="0 0 0 -4"
           VerticalAlignment="Bottom"  HorizontalAlignment="Stretch"/>
    <Thumb Width="7" Height="7" Cursor="SizeNWSE" Margin="-6 -6 0 0"
           VerticalAlignment="Top" HorizontalAlignment="Left"/>
    <Thumb Width="7" Height="7" Cursor="SizeNESW" Margin="0 -6 -6 0"
           VerticalAlignment="Top" HorizontalAlignment="Right"/>
    <Thumb Width="7" Height="7" Cursor="SizeNESW" Margin="-6 0 0 -6"
           VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
    <Thumb Width="7" Height="7" Cursor="SizeNWSE" Margin="0 0 -6 -6"
           VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
   </Grid>
 </ControlTemplate>

<ControlTemplate x:Key="ResizeableDesignerItem" TargetType="ContentControl">
  <Grid>
    <Control Template="{StaticResource ResizeDecoratorTemplate}"
             DataContext="{Binding RelativeSource= {RelativeSource TemplatedParent}}"/>
    <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
  </Grid>
</ControlTemplate>

<Canvas>
  <ContentControl Name="DesignerItem" Template="{StaticResource ResizeableDesignerItem}"
                  Width="130" Height="130" Canvas.Top="100" Canvas.Left="100">
    <Ellipse Fill="Blue" IsHitTestVisible="False"/>
  </ContentControl>
</Canvas>        

We have created a new template that consists of a Grid filled up with a bunch of 8 Thumb controls. By setting the Thumb properties like we did above, we achieved a layout that results in something that looks like a real resize decorator:

A resize decorator build with 8 Thumbs

Amazing, isn't it. But so far it is only a fake, because there is no event handler that handles the DragDelta events when fired. Don't worry, though. That is no big deal, either:

 using System;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace DragAndResizeControl
{
    public class ResizeThumb : Thumb
    {
        private ResizerPosition position;
        public ResizerPosition Position
        {
            get { return position; }
            set { position = value; }
        }

        public ResizeThumb()
        {
            base.DragDelta += new DragDeltaEventHandler(ResizeThumb_DragDelta);
        }

        void ResizeThumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            ContentControl item = this.DataContext as ContentControl;

            if (item != null)
            {
                switch (this.Position)
                {
                    case ResizerPosition.Top:
                        Canvas.SetTop(item, Canvas.GetTop(item) + e.VerticalChange);
                        item.Height = Math.Max(25, item.ActualHeight - e.VerticalChange);
                        break;
                    case ResizerPosition.Bottom:
                        item.Height = Math.Max(25, item.ActualHeight + e.VerticalChange);
                        break;
                    case ResizerPosition.Left:
                        Canvas.SetLeft(item, Canvas.GetLeft(item) + 
                            e.HorizontalChange);
                        item.Width = Math.Max(25, item.ActualWidth - e.HorizontalChange);
                        break;
                    case ResizerPosition.Right:
                        item.Width =  
                                Math.Max(25, item.ActualWidth + e.HorizontalChange);
                        break;
                    case ResizerPosition.TopLeft:
                        Canvas.SetTop(item, Canvas.GetTop(item) + e.VerticalChange);
                        item.Height = Math.Max(25, item.ActualHeight - e.VerticalChange);
                        Canvas.SetLeft(item, Canvas.GetLeft(item) + e.HorizontalChange);
                        item.Width = 
                            Math.Max(25, item.ActualWidth - e.HorizontalChange);
                        break;
                    case ResizerPosition.TopRight:
                        Canvas.SetTop(item, Canvas.GetTop(item) + e.VerticalChange);
                        item.Height = Math.Max(25, item.ActualHeight - e.VerticalChange);
                        item.Width = Math.Max(25, item.ActualWidth + e.HorizontalChange);
                        break;
                    case ResizerPosition.BottomLeft:
                        item.Height = Math.Max(25, item.ActualHeight + e.VerticalChange);
                        Canvas.SetLeft(item, Canvas.GetLeft(item) + 
                            e.HorizontalChange);
                        item.Width = Math.Max(25, item.ActualWidth - e.HorizontalChange);
                        break;
                    case ResizerPosition.BottomRight:
                        item.Height = Math.Max(25, item.ActualHeight + e.VerticalChange);
                        item.Width = Math.Max(25, item.ActualWidth + e.HorizontalChange);
                        break;
                    default:
                        break;
                }
            }
            e.Handled = true;
        }
    }

    public enum ResizerPosition
    {
        None,
        Top,
        Bottom,
        Left,
        Right,
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight
    }
}

The ResizeThumb class defines a Position property which identifies its relative position within ResizeDecoratorTemplate. Remember that we have defined 8 DragThumbs in the Template - four at each corner and four on each side - so each DragThumb has to handle the DragDelta event differently, depending on its position.

For example: if we drag the thumb in the bottom right corner, we have to update only the width and height of the designer item. However, if we drag the thumb in the top-left position, we have to update the position, height and width.

Update: meanwhile I have found that it is possible to detect the relative position of a ResizeThumb with its VerticalAlignment and HorizontalAlignment property. So, you can skip the Position property and the ResizerPosition enumeration. You'll find that update in the attached code. Here in this article, I will keep the old version for illustration purposes.

Now let's update the ResizeDecoratorTemplate:

 <ControlTemplate x:Key="ResizeDecoratorTemplate" TargetType="{x:Type Control}">
  <Grid>
    <s:ResizeThumb Height="3" Cursor="SizeNS" Margin="0 -4 0 0"
                   Position="Top"  VerticalAlignment="Top"
                   HorizontalAlignment="Stretch"/>
    <s:ResizeThumb Width="3" Cursor="SizeWE" Margin="-4 0 0 0"
                   Position="Left" VerticalAlignment="Stretch"
                   HorizontalAlignment="Left"/>
    <s:ResizeThumb Width="3" Cursor="SizeWE" Margin="0 0 -4 0"
                   Position="Right" VerticalAlignment="Stretch"
                   HorizontalAlignment="Right"/>
    <s:ResizeThumb Height="3" Cursor="SizeNS" Margin="0 0 0 -4"
                   Position="Bottom" VerticalAlignment="Bottom"
                   HorizontalAlignment="Stretch"/>
    <s:ResizeThumb Width="7" Height="7" Cursor="SizeNWSE" Margin="-6 -6 0 0"
                   Position="TopLeft" VerticalAlignment="Top"
                   HorizontalAlignment="Left"/>
    <s:ResizeThumb Width="7" Height="7" Cursor="SizeNESW" Margin="0 -6 -6 0"
                   Position="TopRight" VerticalAlignment="Top"
                   HorizontalAlignment="Right"/>
    <s:ResizeThumb Width="7" Height="7" Cursor="SizeNESW" Margin="-6 0 0 -6"
                   Position="BottomLeft" VerticalAlignment="Bottom"
                   HorizontalAlignment="Left"/>
    <s:ResizeThumb Width="7" Height="7" Cursor="SizeNWSE" Margin="0 0 -6 -6"
                   Position="BottomRight" VerticalAlignment="Bottom"
                   HorizontalAlignment="Right"/>
  </Grid>
</ControlTemplate>

<ControlTemplate x:Key="ResizeableDesignerItem" TargetType="ContentControl">
  <Grid>
    <Control Template="{StaticResource ResizeDecoratorTemplate}"
             DataContext="{Binding RelativeSource= {RelativeSource TemplatedParent}}"/>
    <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
  </Grid>
</ControlTemplate>

<Canvas>
  <ContentControl Name="DesignerItem" Template="{StaticResource ResizeableDesignerItem}"
                  Width="130" Height="130" Canvas.Top="100" Canvas.Left="100">
    <Ellipse Fill="Blue" IsHitTestVisible="False"/>
  </ContentControl>
</Canvas>     

Perfect, now we have a working resize decorator.

Rotatable Designer Item

Although the MSDN documentation doesn't claim that the Thumb control lets the user rotate controls, we will give it a try and follow the same schema as with did with the ResizeDecorator. Again, we assemble a bunch of Thumbs and do some layouting:

 <ControlTemplate x:Key="RotateDecoratorTemplate" TargetType="{x:Type Control}">
  <Grid>
    <s:RotateThumb Margin="-18,-18,0,0" VerticalAlignment="Top" 
            HorizontalAlignment="Left"/>
    <s:RotateThumb Margin="0,-18,-18,0" VerticalAlignment="Top" 
            HorizontalAlignment="Right">
      <s:RotateThumb.RenderTransform>
        <RotateTransform Angle="90"/>
      </s:RotateThumb.RenderTransform>
    </s:RotateThumb>
    <s:RotateThumb Margin="0,0,-18,-18" VerticalAlignment="Bottom" 
            HorizontalAlignment="Right">
      <s:RotateThumb.RenderTransform>
        <RotateTransform Angle="180" />
      </s:RotateThumb.RenderTransform>
    </s:RotateThumb>
    <s:RotateThumb Margin="-18,0,0,-18" VerticalAlignment="Bottom" 
            HorizontalAlignment="Left">
      <s:RotateThumb.RenderTransform>
        <RotateTransform Angle="270" />
      </s:RotateThumb.RenderTransform>
    </s:RotateThumb>
  </Grid>
  </ControlTemplate>

Instead of using the default Thumb style, we define a new one:

 <Style TargetType="{x:Type s:RotateThumb}">
  <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
  <Setter Property="Cursor" Value="Hand"/>
  <Setter Property="Control.Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type s:RotateThumb}">
        <Grid Width="30" Height="30">
          <Path Fill="#AAD0D0DD" Stretch="Fill" 
                  Data="M 50,100 A 50,50 0 1 1 100,50 H 50 V 100"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>            

Here you see how the RotateDecorator, together with the ResizeDecorator, looks:

A rotate decorator build with 4 Thumbs

Now we have to provide the code for the RotateThumb class. This time we do not only handle the DragDelta event, but also the DragStart event, which allows us to initialize each rotate operation in the following way:

 void RotateThumb_DragStarted(object sender, DragStartedEventArgs e)
{
    Canvas canvas = VisualTreeHelper.GetParent(DesignerItem) as Canvas;
    if (DesignerItem != null && canvas != null)
    {                
        // the RenderTransformOrigin property of DesignerItem defines
        // transformation center relative to its bounds
        centerPoint = DesignerItem.TranslatePoint(
            new Point(DesignerItem.Width * DesignerItem.RenderTransformOrigin.X,
                      DesignerItem.Height * DesignerItem.RenderTransformOrigin.Y),
            canvas);

        // calculate startVector, that is the vector from centerPoint to startPoint
        Point startPoint = Mouse.GetPosition(canvas);
        startVector = Point.Subtract(startPoint, centerPoint);

        // check if the DesignerItem already has a RotateTransform set ...
        RotateTransform rotateTransform = 
                DesignerItem.RenderTransform as RotateTransform;
        if (rotateTransform == null)
        {
            // if not we create one with zero angle 
            DesignerItem.RenderTransform = new RotateTransform(0);
            initialAngle = 0;
        }
        else
            initialAngle = rotateTransform.Angle;
    }
}  

Whenever a rotate operation is started, we calculate the centerPoint and the startVector. The centerPoint is the center of our transform and the startVector is the vector reaching from the centerPoint to the current mouse position. After that, we check if the DesignerItem already has assigned a RotateTranform. If so, we cache the current rotate angle. Otherwise, we assign a new RotateTranform with angle zero.

 void RotateThumb_DragDelta(object sender, DragDeltaEventArgs e)
{s = VisualTreeHelper.GetParent(DesignerItem) as Canvas;

    if (DesignerItem != null && canvas != null)
    {
        //we calculate the angle between startVector and dragVector
        Point currentPoint = Mouse.GetPosition(canvas);
        Vector deltaVector = Point.Subtract(currentPoint, centerPoint);
        double angle = Vector.AngleBetween(startVector, deltaVector);

        // and update the transformation
        RotateTransform rotateTransform = 
                DesignerItem.RenderTransform as RotateTransform;
        rotateTransform.Angle = initialAngle + Math.Round(angle, 0);
    }
} 

Every time the DragDelta event is fired, we just have to calculate the angle between the startVector and a vector called deltaVector, which is the vector reaching from the centerPoint to the actual mouse position. Finally, we update the RenderTransform property and that's it - almost. There is still something we have to consider, namely the fact that when a DesignerItem is rotated, the drag and resize operations also have to consider this in their calculations. How this works please see in the code, but if you have questions please ask them and I will do my best to answer them.

Drag, Resize and Rotate Designer Item

What is left to be done is to merge everything into a style:

 <Style x:Key="DesignerItemStyle" TargetType="ContentControl">
  <Setter Property="MinHeight" Value="50"/>
  <Setter Property="MinWidth" Value="50"/>
  <Setter Property="SnapsToDevicePixels" Value="true"/>
  <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ContentControl">
        <Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
          <Control Template="{StaticResource RotateDecoratorTemplate}"/>
          <s:DragThumb Template="{StaticResource DragThumbTemplate}" Cursor="SizeAll"/>
          <Control Template="{StaticResource ResizeDecoratorTemplate}"/>
          <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
        </Grid>          
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>ter>

One has to realize that these few lines of XAML code together with DragThumb, ResizeThumb and RotateThumb provide all we need to drag, resize and rotate elements on a canvas! Best of all, we don't need to touch the element itself: all the behaviour is completely separated in a Template.

Outlook

In the next article, I will cover the following issues:

  • Scrollable designer canvas
  • Selectable designer items
  • Drag designer items from a toolbox and drop them on the canvas

History

  • 10 January, 2008 -- Original version
  • 18 January, 2008 -- Update: use of ContentControl as designer item instead of Control
  • 5 February, 2008 -- Update: added rotation of elements

License

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

About the Author

sukram


My name is not sukram
My name is Markus



Location: Austria Austria

Other popular Windows Presentation Foundation articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 11 of 11 (Total in Forum: 11) (Refresh)FirstPrevNext
Subject  Author Date 
QuestionGreat Article! Could everything be placed in an Adorner?membernonsequitoria5:45 13 Mar '08  
GeneralRe: Great Article! Could everything be placed in an Adorner?membersukram10:35 13 Mar '08  
GeneralLayout AlgorythmsmemberAndrew Lush13:13 25 Feb '08  
GeneralRe: Layout Algorythmsmembersukram10:23 27 Feb '08  
GeneralIf DesignerItem.RenderTransform is TransformGroup ??memberAjas0:36 8 Feb '08  
GeneralRe: If DesignerItem.RenderTransform is TransformGroup ??membersukram7:50 11 Feb '08  
GeneralCool , how to add RotationThumb combine with ResizeThumb?memberwoposmail3:57 29 Jan '08  
GeneralRe: Cool , how to add RotationThumb combine with ResizeThumb?member