Click here to Skip to main content
14,608,242 members

WPF Diagram Designer - Part 2

Rate this:
4.96 (143 votes)
Please Sign up or sign in to vote.
4.96 (143 votes)
8 Oct 2008CPOL
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.

Image 3

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:

Image 4

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:

Image 5 


  • 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

GeneralUnable to rotate object Pin
sosillysally20-Feb-09 13:55
Membersosillysally20-Feb-09 13:55 
GeneralPlace of the Zoombox Pin
Quentschi9-Jan-09 1:44
MemberQuentschi9-Jan-09 1:44 
Generalnice! Pin
viciouskinid30-Nov-08 22:52
Memberviciouskinid30-Nov-08 22:52 
QuestionHow to detect drop of one ToolBoxItem on another Pin
Member 273853524-Nov-08 16:05
MemberMember 273853524-Nov-08 16:05 
AnswerPrinting works thanks to the MeasureOverride method (printVisual) Pin
nitropit26-Aug-08 20:55
Membernitropit26-Aug-08 20:55 
muneeb1931-Aug-08 21:30
Membermuneeb1931-Aug-08 21:30 
sukram27-Aug-08 0:02
Membersukram27-Aug-08 0:02 
muneeb1931-Aug-08 21:21
Membermuneeb1931-Aug-08 21:21 
sukram27-Aug-08 0:02
Membersukram27-Aug-08 0:02 
GeneralSilverlight App Pin
fiannolo27-Jul-08 12:30
Memberfiannolo27-Jul-08 12:30 
GeneralRe: Silverlight App Pin
sukram27-Aug-08 0:21
Membersukram27-Aug-08 0:21 
GeneralRe: Silverlight App Pin
mtonsager25-Oct-09 5:17
Membermtonsager25-Oct-09 5:17 
GeneralRe: Silverlight App Pin
anilmomin8713-Jul-10 1:37
Memberanilmomin8713-Jul-10 1:37 
GeneralRubberbandAdorner preformance issue Pin
Jakub Klímek12-May-08 22:12
MemberJakub Klímek12-May-08 22:12 
GeneralRe: RubberbandAdorner preformance issue Pin
sukram14-May-08 23:53
Membersukram14-May-08 23:53 
GeneralRe: RubberbandAdorner preformance issue [modified] Pin
Jakub Klímek28-May-08 20:15
MemberJakub Klímek28-May-08 20:15 
GeneralRubberbandAdorner preformance issue fixed [modified] Pin
sukram27-Aug-08 0:04
Membersukram27-Aug-08 0:04 
QuestionRotating [modified] Pin
KBou27-Mar-08 3:28
MemberKBou27-Mar-08 3:28 
GeneralRe: Rotating Pin
sukram27-Mar-08 8:20
Membersukram27-Mar-08 8:20 
> Is this why you've left it out?

Yes, sort of. Initially rotation wasn't planned.
GeneralRe: Rotating Pin
KBou27-Mar-08 22:16
MemberKBou27-Mar-08 22:16 
GeneralVery Nice Pin
jherington19-Feb-08 7:03
Memberjherington19-Feb-08 7:03 
GeneralThis might give you additional ideas Pin
Denis Vuyka10-Feb-08 21:51
MemberDenis Vuyka10-Feb-08 21:51 
GeneralRe: This might give you additional ideas Pin
lyntonhu11-Feb-08 14:28
Memberlyntonhu11-Feb-08 14:28 
GeneralNice work!! I can't wait to see part -3 ! Please post it asap. Pin
lyntonhu10-Feb-08 21:00
Memberlyntonhu10-Feb-08 21:00 
GeneralNice start Pin
Sacha Barber8-Feb-08 1:57
MemberSacha Barber8-Feb-08 1:57 

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

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

Posted 28 Jan 2008


336 bookmarked