5,138,331 members and growing! (12,805 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 2

By sukram

First simple Diagram Designer Application
C# (C# 3.0, C#), XML, .NET (.NET, .NET 3.5), XAML, WPF, Dev

Posted: 28 Jan 2008
Updated: 11 Feb 2008
Views: 14,865
Announcements



Search    
Advanced Search
Sitemap
38 votes for this Article.
Popularity: 7.45 Rating: 4.72 out of 5
2 votes, 5.3%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
3 votes, 7.9%
4
33 votes, 86.8%
5
WPF Diagram Designer

Last Update

11 February, 2008: Rubberband selection

Introduction

In the first article I have shown you how to drag, resize and rotate elements on a canvas. Today we are going to add further features that are essential for a typical diagram designer:

  • Rubberband selection
  • Multiple selection with keystrokes (LeftMouseButton + Ctrl, Shift)
  • Scrollable designer canvas
  • Toolbox with drag & drop

Using the Code

Besides the main DiagramDesigner project, the attached source includes a second project named DiagramDesignerBasic, which is just a stripped down version to make everything easier to read and understand.

WPF Diagram Designer

WPF is really amazing. A few classes together with some XAML code, that is all you need for this demo.

DesignerCanvas

The DesignerCanvas is directly inherited from Canvas and will be used as the background panel for our diagram. It has a SelectedItems property of type List<DesignerItem> which is used to keep track of the selected items.

 public class DesignerCanvas : Canvas
 {
     private List<designeritem> selectedItems;
     public List<designeritem> SelectedItems
     {
         get { return selectedItems; }
         set { selectedItems = value; }
     }

        ...
 } 

In the demo of my previous article, you probably have noticed one special shortcoming: when you drag an item outside the DesignerCanvas the item is no longer accessible. How can we change this? For the top and left side of the canvas we could simply prohibit to drag an item outside those borders, but that is not acceptable for the right and bottom border. The normal behaviour in such cases would be that the canvas provides scroll bars so that you can easily scroll to any item.

Wrapping the DesignerCanvas into a ScrollViewer is the way to go, but it's only half the battle. Each time we drag an item the DesignerCanvas should notify the ScrollViewer of the size it desires, but obviously it doesn't do it by default. We find the solution in the MeassureOverride method defined by FrameworkElement which enables an element to return its desired size. Thus the MeasureOverride method allows the DesignerCanvas to calculate its desired size and return it to the WPF layout system. For our DesignerCanvas the desired size is identical to the size of a bounding box that includes all its children.

 protected override Size MeasureOverride(Size constraint)
 {
     Size size = new Size();
     foreach (UIElement element in base.Children)
     {
         double left = Canvas.GetLeft(element);
         double top = Canvas.GetTop(element);
         left = double.IsNaN(left) ? 0 : left;
         top = double.IsNaN(top) ? 0 : top;

         //measure desired size for each child
         element.Measure(constraint);

         Size desiredSize = element.DesiredSize;
         if (!double.IsNaN(desiredSize.Width) && !double.IsNaN(desiredSize.Height))
         {
             size.Width = Math.Max(size.Width, left + desiredSize.Width);
             size.Height = Math.Max(size.Height, top + desiredSize.Height);
         }
     }
     //for aesthetic reasons add extra points
     size.Width += 10;
     size.Height += 10;
     return size;
 }

And finally the DesignerCanvas has to handle the MouseDown event so that all selected items are deselected when a user clicks on the canvas itself.

 protected override void OnMouseDown(MouseButtonEventArgs e)
 {
     base.OnMouseDown(e);
     if (e.Source == this)
     {
         this.dragStartPoint = new Point?(e.GetPosition(this));

         foreach (DesignerItem item in selectedItems)
              item.IsSelected = false;
         selectedItems.Clear();

         e.Handled = true;
     }
 } 

DesignerItem

The DesignerItem is inherited from ContentControl, so that we can reuse the ControlTemplate of our first article. The DesignerItem provides an IsSelected property to signal if the item is selected or not:

 public class DesignerItem : ContentControl
 {

     public bool IsSelected
     {
         get { return (bool)GetValue(IsSelectedProperty); }
         set { SetValue(IsSelectedProperty, value); }
     }
     public static readonly DependencyProperty IsSelectedProperty =
        DependencyProperty.Register("IsSelected", typeof(bool),
                                     typeof(DesignerItem),
                                     new FrameworkPropertyMetadata(false));
        ...

 }

Then we have to implement an event handler for the MouseDown event to support multi selection:

 protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
 {
     base.OnPreviewMouseDown(e);
     DesignerCanvas designer = VisualTreeHelper.GetParent(this) as DesignerCanvas;

     if (designer != null)
         if ((Keyboard.Modifiers & (ModifierKeys.Shift | ModifierKeys.Control))
                != ModifierKeys.None)
             if (this.IsSelected)
             {
                 this.IsSelected = false;
                 designer.SelectedItems.Remove(this);
             }
             else
             {
                 this.IsSelected = true;
                 designer.SelectedItems.Add(this);
             }
         else if (!this.IsSelected)
         {
             foreach (DesignerItem item in designer.SelectedItems)
                 item.IsSelected = false;

             designer.SelectedItems.Clear();
             this.IsSelected = true;
             designer.SelectedItems.Add(this);
         }
     e.Handled = false;
 }

Please note that we handle the PreviewMouseDown event, which is the tunnelling version of the MouseDown event and that we mark the event as not handled. The reason is that we want the item to be selected even when the MouseDown event is targeting another Control within the DesignerItem, e.g. take a look at a class diagram in Visual Studio, if you click on the ToggleButton of the Expander the item becomes selected and the Expander toggles its size, both at the same time.

WPF Diagram Designer

Now we have to update the template for the DesignerItem such that the resize decorator is only visible when the IsSelected property is true, which can be managed with a simple DataTrigger:

  <!-- DesignerItem Style -->
  <Style TargetType="{x:Type s:DesignerItem}">
    <Setter Property="MinWidth" Value="25"/>
    <Setter Property="MinHeight" Value="25"/>
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type s:DesignerItem}">
          <Grid DataContext="{Binding RelativeSource=
                {RelativeSource TemplatedParent}}">
            <!-- PART_DragThumb -->
            <c:DragThumb x:Name="PART_DragThumb" Cursor="SizeAll"/>
            <!-- PART_ResizeDecorator -->
            <Control x:Name="PART_ResizeDecorator"
                     Visibility="Collapsed"
                     Template="{StaticResource ResizeDecoratorTemplate}"/>

            <!-- PART_ContentPresenter -->
            <ContentPresenter x:Name="PART_ContentPresenter"
                              Content="{TemplateBinding ContentControl.Content}"
                              Margin="{TemplateBinding ContentControl.Padding}"/>
          </Grid>
          <ControlTemplate.Triggers>
            <DataTrigger Value="True" Binding=
                "{Binding RelativeSource={RelativeSource Self},Path=IsSelected}">
              <Setter TargetName="PART_ResizeDecorator" Property="Visibility"
                    Value="Visible"/>
            </DataTrigger>
          </ControlTemplate.Triggers>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

Toolbox

The Toolbox is an ItemsControl that should use the ToolboxItem class to display the items. For this we have to override the GetContainerForItemOverride method and the IsItemItsOwnContainerOverride method.

public class Toolbox : ItemsControl
{
    private Size itemSize = new Size(65, 65);
    public Size ItemSize
    {
        get { return itemSize; }
        set { itemSize = value; }
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ToolboxItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return (item is ToolboxItem);
    }
}

Additionally we want the Toolbox to use a WrapPanel to layout its items. Also note that the ItemHeight and ItemWidth properties of the WrapPanel are bound to the ItemSize property of the Toolbox.

<Setter Property="ItemsPanel">
   <Setter.Value>
     <ItemsPanelTemplate>
       <WrapPanel Margin="0,5,0,5"
                  ItemHeight="{Binding Path=ItemSize.Height,
                        RelativeSource={RelativeSource AncestorType=s:Toolbox}}"
                  ItemWidth="{Binding Path=ItemSize.Width,
                        RelativeSource={RelativeSource AncestorType=s:Toolbox}}"/>
     </ItemsPanelTemplate>
   </Setter.Value>
</Setter>

ToolboxItem

The ToolboxItem finally is the place where drag operations are started. There is nothing mysterious about drag and drop itself, but still you have to take care how to copy an item from the drag source (Toolbox) to the drop target (DesignerCanvas). Here we use the XamlWriter.Save method to serialize the content of the ToolboxItem into XAML, although that kind of serialization has some notable limitations in exactly what is serialized. In a later article, we will switch to binary serialization.

 public class ToolboxItem : ContentControl
 {
     private Point? dragStartPoint = null;

     static ToolboxItem()
     {
         FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(ToolboxItem),
                new FrameworkPropertyMetadata(typeof(ToolboxItem)));
     }

     protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
     {
         base.OnPreviewMouseDown(e);
         this.dragStartPoint = new Point?(e.GetPosition(this));
     }

     protected override void OnMouseMove(MouseEventArgs e)
     {
         base.OnMouseMove(e);
         if (e.LeftButton != MouseButtonState.Pressed)
         {
             this.dragStartPoint = null;
         }
         if (this.dragStartPoint.HasValue)
         {
             Point position = e.GetPosition(this);
             if ((SystemParameters.MinimumHorizontalDragDistance <=
                  Math.Abs((double)(position.X - this.dragStartPoint.Value.X))) ||
                  (SystemParameters.MinimumVerticalDragDistance <=
                  Math.Abs((double)(position.Y - this.dragStartPoint.Value.Y))))
             {
                 string xamlString = XamlWriter.Save(this.Content);
                 DataObject dataObject = new DataObject("DESIGNER_ITEM", xamlString);

                 if (dataObject != null)
                 {
                     DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Copy);
                 }
             }
             e.Handled = true;
         }
     }
 }

Rubberband Selection

Rubberband selection is best handled with an Adorner class:

public class RubberbandAdorner : Adorner
{
    private Point? startPoint;
    private Point? endPoint;

    ...
}

The RubberbandAdorner has two important private field members - the startPoint and the endPoint. The startPoint caches the point where the drag operation has started and the endPoint holds the current mouse position. But where and when is the RubberbandAdorner created? When a user starts a drag operation directly on the DesignerCanvas its OnMouseMove creates a RubberbandAdorner in the following way:

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

    if (e.LeftButton != MouseButtonState.Pressed)
        this.dragStartPoint = null;

    if (this.dragStartPoint.HasValue)
    {
        AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this);
        if (adornerLayer != null)
        {
            RubberbandAdorner adorner = new RubberbandAdorner(this, dragStartPoint);
            if (adorner != null)
            {
                adornerLayer.Add(adorner);
            }
        }
    }

    e.Handled = true;
}

An adorner is always on top of the adorned element and thus always has a higher z-order. As a consequence the RubberbandAdorner can handle all the input events it has to handle, like MouseMove:

protected override void OnMouseMove(System.Windows.Input.MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        if (!this.IsMouseCaptured)
            this.CaptureMouse();

        endPoint = e.GetPosition(this);
        UpdateSelection();
        this.InvalidateVisual();
    }
    else
    {
        if (this.IsMouseCaptured) this.ReleaseMouseCapture();
    }

    e.Handled = true;
}

While the mouse button is pressed, the endPoint is updated with the actual value of the mouse position. This is followed by an update of the selected items:

private void UpdateSelection()
{
    foreach (DesignerItem item in designerCanvas.SelectedItems)
        item.IsSelected = false;
    designerCanvas.SelectedItems.Clear();

    Rect rubberband = new Rect(startPoint.Value, endPoint.Value);
    foreach (Control child in designerCanvas.Children)
    {
        DesignerItem item = child as DesignerItem;
        if (item != null)
        {
            // I LOVE WPF
            Rect itemRect = VisualTreeHelper.GetDescendantBounds(item);
            Rect itemBounds = item.TransformToAncestor
                    (designerCanvas).TransformBounds(itemRect);

            if (rubberband.Contains(itemBounds))
            {
                item.IsSelected = true;
                designerCanvas.SelectedItems.Add(item);
            }
        }
    }
}

After cleaning the current selection I create the actual rubberband rectangle and then I check for each DesignerItem if it is contained in that rubberband. And here is where WPF shines: The VisualTreeHelper.GetDescendantBounds(item) method provides the bounding rectangle for the DesignerItem and then the coordinates of this rectangle are transformed to the DesignerCanvas. Now we just have to call the rubberband.Contains(itemBounds) method to get the answer!

Customize the DragThumb

The default style of the DragThumb class is a transparent Rectangle, but if you want to adjust that style you can do this with the help of an attached property named DesignerItem.DragThumbTemplate. Let me explain the usage with an example. Let's say the content of a DesignerItem is a star shape like this one:

<Path Stroke="Red" StrokeThickness="5" Stretch="Fill" IsHitTestVisible="false"
      Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z"/> 

To illustrate the result I have colorized the default DragThumb template:

Image

Now try the following:

 <Path Stroke="Red" StrokeThickness="5" Stretch="Fill" IsHitTestVisible="false"
      Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z">
    <s:DesignerItem.DragThumbTemplate>
         <ControlTemplate>
            <Path Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z"
                    Fill="Transparent" Stretch="Fill"/>
         </ControlTemplate>
    </s:DesignerItem.DragThumbTemplate>
</Path>

The result is a DragThumb that fits much better than the default one:

Image

Outlook

In the coming article, I will show you how to connect DesignerItems, here is a little teaser:

Image

Credits

Most of the control styles are taken from the WPF SDK samples.

History

  • 28 January, 2008 -- Original version submitted
  • 11 February, 2008 -- Rubberband selection added

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 20 of 20 (Total in Forum: 20) (Refresh)FirstPrevNext
Subject  Author Date 
GeneralRubberbandAdorner preformance issuememberJakub Klímek23:12 12 May '08  
GeneralRe: RubberbandAdorner preformance issuemembersukram0:53 15 May '08  
QuestionRotating [modified]memberKBou4:28 27 Mar '08  
GeneralRe: Rotatingmembersukram9:20 27 Mar '08  
GeneralRe: RotatingmemberKBou23:16 27 Mar '08  
GeneralVery Nicememberjherington8:03 19 Feb '08  
GeneralThis might give you additional ideasmemberVuyka22:51 10 Feb '08  
GeneralRe: This might give you additional ideasmemberlyntonhu15:28 11 Feb '08  
GeneralNice work!! I can't wait to see part -3 ! Please post it asap.memberlyntonhu22:00 10 Feb '08  
GeneralNice startmvpSacha Barber2:57 8 Feb '08  
GeneralAmazing stuffmemberjulius-dias5:03 7 Feb '08  
GeneralRe: Amazing stuffmembersukram8:30 7 Feb '08  
GeneralRe: Amazing stuffmemberAjithIsKool23:04 7 Feb '08  
AnswerRe: Amazing stuffmembersukram23:35 7 Feb '08  
GeneralGood stuffmemberFatGeek3:42 29 Jan '08  
GeneralnicememberAbhijit Jana18:52 28 Jan '08  
GeneralGreat article.memberRajib Ahmed9:48 28 Jan '08  
GeneralRe: Great article.membersukram23:15 28 Jan '08  
GeneralRe: Great article.memberRajib Ahmed18:12 30 Jan '08  
GeneralRe: Great article.membersukram7:49 31 Jan '08  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 11 Feb 2008
Editor: Deeksha Shenoy
Copyright 2008 by sukram
Everything else Copyright © CodeProject, 1999-2008
Web15 | Advertise on the Code Project