Click here to Skip to main content
Click here to Skip to main content

WPF Diagram Designer - Part 2

, 8 Oct 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
Designer Canvas with Zoombox
WPF Diagram Designer

Last Update

  • Zoombox (new)
  • Rubberband Adorner (updated)
WPF Zoombox


In the first article of this series, I have shown you how to move, resize and rotate items on a canvas. This time we are going to add further features that are essential for a typical diagram designer:

  • Designer Canvas (variable size, scrollable)
  • Zoombox
  • Rubberband selection
  • Keystroke selection (LeftMouseButton + Ctrl)
  • Toolbox (drag & drop)
  • Rotate items (left, right)

Designer Canvas

In the previous article, you probably have noticed that when you move an item outside the borders of the DesignerCanvas the item is no longer accessible. Normally you would expect that the designer application provides scroll bars so that you can easily scroll to any item outside the visible canvas region. For this I thought I just have to wrap the DesignerCanvas into a ScrollViewer, but that didn't work. I soon found the reason for this behaviour; let me explain it with the following code snippet:

 <Canvas Width="200"
    <Rectangle Fill="Blue"
               Canvas.Top="300" />

Here I have placed a Rectangle object on a Canvas, but positioned it outside the boundaries of the Canvas. Will this change the size of the Canvas? Of course not, the Canvas will keep its size, no matter where you place an item.

For the DesignerCanvas this means that it will keep its size, even if you drag an item far beyond the borders of the canvas. Now we understand why a ScrollViewer doesn't help: the DesignerCanvas will never notify the ScrollViewer of a size change, just because there is none.

The solution is that we must force the DesignerDanvas to adjust its size everytime an item is moved or resized. Fortunately the Canvas class provides an overrideable method named MeassureOverride that allows the DesignerCanvas to calculate its desired size and return it to the WPF layout system. The calculation is quite simple as you can see here:

 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

        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;


The DesignerItem is inherited from ContentControl, so that we can reuse the ControlTemplate of our first article. The DesignerItem provides an IsSelected property to indicate if it 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),
                                    new FrameworkPropertyMetadata(false));


Then we have to implement an event handler for the MouseDown event to support multiple selection of items:

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

    if (designer != null)
        if ((Keyboard.Modifiers & 
		(ModifierKeys.Shift | ModifierKeys.Control)) != ModifierKeys.None)
            this.IsSelected = !this.IsSelected;
            if (!this.IsSelected)
                this.IsSelected = true;

     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 if the MouseDown event is targeting another Control inside 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.

Finally 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 handled with a simple DataTrigger:

  <Style TargetType="{x:Type s:DesignerItem}">
    <Setter Property="MinHeight" Value="50"/>
    <Setter Property="MinWidth" Value="50"/>
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="Template">
        <ControlTemplate TargetType="{x:Type s:DesignerItem}">
          <Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, 
                Template="{StaticResource MoveThumbTemplate}" />
                Content="{TemplateBinding ContentControl.Content}"
                Margin="{TemplateBinding Padding}"/>
            <s:ResizeDecorator x:Name="PART_DesignerItemDecorator"/>
            <Trigger Property="IsSelected" Value="True">
              <Setter TargetName="PART_DesignerItemDecorator" 
			Property="ShowDecorator" Value="True"/>


The Toolbox is an ItemsControl that uses the ToolboxItem class as default container to display its items. For this we have to override the GetContainerForItemOverride method and the IsItemItsOwnContainerOverride method:

public class Toolbox : ItemsControl
    private Size defaultItemSize = new Size(65, 65);
    public Size DefaultItemSize
        get { return this.defaultItemSize; }
        set { this.defaultItemSize = 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:

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

Note that the ItemHeight and ItemWidth properties of the WrapPanel are bound to the DefaultItemSize property of the Toolbox.


The ToolboxItem is the place where drag operations are actually started if you want to drag an item from the toolbox and drop it on the canvas. 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). In our case 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()
                new FrameworkPropertyMetadata(typeof(ToolboxItem)));

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

     protected override void OnMouseMove(MouseEventArgs 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

When the user initiates a drag operation directly on the DesignerCanvas, a new instance of a RubberbandAdorner is created:

public class DesignerCanvas : Canvas

    protected override void OnMouseMove(MouseEventArgs 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)
            e.Handled = true;

As soon as the RubberbandAdorner is created, it takes control over the drag operation and updates the drawing of the rubber band and the current selection of items. These updates happen inside the UpdateRubberband() and UpdateSelection() methods:

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

    protected override void OnMouseMove(MouseEventArgs e)
        if (e.LeftButton == MouseButtonState.Pressed)
            if (!this.IsMouseCaptured)

            this.endPoint = e.GetPosition(this);
            e.Handled = true;

Since the actual rubber band is an instance of a Rectangle class, the UpdateRubberband() method just needs to update the size and the position of that Rectangle:

private void UpdateRubberband()
    double left = Math.Min(this.startPoint.Value.X, this.endPoint.Value.X);
    double top = Math.Min(this.startPoint.Value.Y, this.endPoint.Value.Y);

    double width = Math.Abs(this.startPoint.Value.X - this.endPoint.Value.X);
    double height = Math.Abs(this.startPoint.Value.Y - this.endPoint.Value.Y);

    this.rubberband.Width = width;
    this.rubberband.Height = height;
    Canvas.SetLeft(this.rubberband, left);
    Canvas.SetTop(this.rubberband, top);

A little more work needs to be done in the UpdateSelection() method. Here we check for each DesignerItem if it is contained in the current rubber band. For this, the VisualTreeHelper.GetDescendantBounds(item) method provides us the bounding rectangle for each item. We transform the coordinates of this rectangle to the DesignerCanvas and call the rubberband.Contains(itemBounds) method to decide whether the item is selected or not!

private void UpdateSelection()
    Rect rubberBand = new Rect(this.startPoint.Value, this.endPoint.Value);
    foreach (DesignerItem item in this.designerCanvas.Children)
        Rect itemRect = VisualTreeHelper.GetDescendantBounds(item);
        Rect itemBounds = item.TransformToAncestor

        if (rubberBand.Contains(itemBounds))
            item.IsSelected = true;
            item.IsSelected = false;

Please note that these update methods are called whenever the MouseMove event is fired during a drag operation, and that is quite frequently! Instead you may consider to update the selection only once at the end of the drag operation, when the MouseUp event is fired.

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:

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">
            <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"/>

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



  • 28th January, 2008 -- Original version submitted
  • 11th February, 2008 -- Rubberband selection added
  • 7th October, 2008 -- Zoombox added, RubberbandAdorner updated


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


About the Author


Austria Austria
No Biography provided

Comments and Discussions

QuestionRubberband Selection autoscroll PinmemberAhmad Tariq19-Jun-14 1:32 
GeneralMy vote of 5 PinmemberAdy Shimony10-Dec-12 6:59 
GeneralMy vote of 5 PinmemberChona117126-Sep-12 0:31 
QuestionDo you have a demo with Silverlight? Pinmembervietanh15418-Sep-12 22:35 
QuestionPlz guide, What is exact use of Rubberband selection Pinmemberrahul.uttarkar@igatepatni.com5-Sep-12 21:24 
GeneralMy vote of 5 Pinmemberaghapour.ahad29-Jul-12 19:35 
QuestionHow to Convert in Powerbuilder Script PinmemberJosant Wighuno8-Jun-12 0:28 
Questiongood article PinmemberCIDev25-Apr-12 12:22 
GeneralMy vote of 5 Pinmembersalim4187-Jul-11 2:39 
GeneralCausing scrollbars to appear by acknowledging that the canvas needs to be redrawn PinmemberPat Kujawa17-Dec-10 8:11 
GeneralMy vote of 5 Pinmembersuryawang24-Oct-10 18:23 
GeneralImage Tiling Pinmemberbreath2k6-Sep-10 6:32 
GeneralCanvas Background Image Pinmemberbreath2k6-Sep-10 5:44 
GeneralSaving Pinmemberbreath2k6-Sep-10 5:20 
GeneralSet of icons PinmemberRobson Félix17-Mar-10 6:07 
QuestionPaint Application PinmemberVisionator5-Feb-10 21:45 
GeneralBravo! [EOM] PinmemberTheArchitectmc30-Dec-09 7:49 
GeneralSoftiiiiiiiiiiii Pinmemberms_soft8921-Oct-09 0:10 
GeneralGreat article! PinmemberSELVAM PARAMASIVAN27-Apr-09 16:06 
This is one of the great article and I was thinking to design an pictorial representation of cutom workflow (entity in my current assignment) using flow chart. I beleive this is nice work and great reference for me to build my own.
GeneralUnable to rotate object Pinmembersosillysally20-Feb-09 14:55 
GeneralPlace of the Zoombox PinmemberQuentschi9-Jan-09 2:44 
Generalnice! Pinmemberviciouskinid30-Nov-08 23:52 
QuestionHow to detect drop of one ToolBoxItem on another PinmemberMember 273853524-Nov-08 17:05 
AnswerPrinting works thanks to the MeasureOverride method (printVisual) Pinmembernitropit26-Aug-08 21:55 
QuestionHOW TO DELETE THE ADDED THUMB OR CONNECTOR LINE Pinmembermuneeb1931-Aug-08 22:30 
AnswerRe: HOW TO DELETE THE ADDED THUMB OR CONNECTOR LINE Pinmembersukram27-Aug-08 1:02 
GeneralSilverlight App Pinmemberfiannolo27-Jul-08 13:30 
GeneralRe: Silverlight App Pinmembersukram27-Aug-08 1:21 
GeneralRe: Silverlight App Pinmembermtonsager25-Oct-09 6:17 
GeneralRe: Silverlight App Pinmemberanilmomin8713-Jul-10 2:37 
GeneralRubberbandAdorner preformance issue PinmemberJakub Klímek12-May-08 23:12 
GeneralRe: RubberbandAdorner preformance issue Pinmembersukram15-May-08 0:53 
GeneralRe: RubberbandAdorner preformance issue [modified] PinmemberJakub Klímek28-May-08 21:15 
GeneralRubberbandAdorner preformance issue fixed [modified] Pinmembersukram27-Aug-08 1:04 
QuestionRotating [modified] PinmemberKBou27-Mar-08 4:28 
GeneralRe: Rotating Pinmembersukram27-Mar-08 9:20 
GeneralRe: Rotating PinmemberKBou27-Mar-08 23:16 
GeneralVery Nice Pinmemberjherington19-Feb-08 8:03 
GeneralThis might give you additional ideas PinmemberVuyka10-Feb-08 22:51 
GeneralRe: This might give you additional ideas Pinmemberlyntonhu11-Feb-08 15:28 
GeneralNice work!! I can't wait to see part -3 ! Please post it asap. Pinmemberlyntonhu10-Feb-08 22:00 
GeneralNice start PinmvpSacha Barber8-Feb-08 2:57 
GeneralAmazing stuff Pinmemberjulius-dias7-Feb-08 5:03 
GeneralRe: Amazing stuff Pinmembersukram7-Feb-08 8:30 
GeneralRe: Amazing stuff PinmemberAjithIsKool7-Feb-08 23:04 
AnswerRe: Amazing stuff Pinmembersukram7-Feb-08 23:35 
GeneralGood stuff PinmemberFatGeek29-Jan-08 3:42 
Generalnice PinmemberAbhijit Jana28-Jan-08 18:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150327.1 | Last Updated 8 Oct 2008
Article Copyright 2008 by sukram
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid