WPFDesigner






4.24/5 (14 votes)
A WPF UserControl on which you can add elements and move/resize them
Introduction
This article describes how to create a Windows Presentation Foundation (WPF) UserControl
on which you can add elements and move/resize them.

Background
Because WPF doesn't have a designer component by default - something that allows to add controls on it and move/resize them - one must be created. There are many solutions for this problem but as far as I know, there is no one that provides the ability to add/remove controls without manually wrapping them in custom control.
A Birdeye View
WPF Designer is basically a canvas on which you can add anything that derives from FrameworkElement
.
When a control is added, it is not placed directly on the canvas but instead is wrapped in another container. The container has all the handles and logic for moving and resizing.
The end is not disturbed by this control because the designer exposes all the controls in it as a FrameworkElement
collection.
If an element that is added in the designer has a fixed dimension (width/height), there will be no handles for that one. Min/max limits are also treated so you cannot resize a control more that its initial specifications.
The inner control is placed in a ContentPresenter
holder.

Tunneling or Bubbling?
One of the big questions I had to answer when developing this control was "should I use tunneling of bubbling when clicking the control?". Tunneling is when an event is routed from parent to child and bubbling is the opposite.
Because in WPF an event can be marked as Handled
, I've decided to use tunneling (events that contain "Preview" in their names). For example, when the user clicks an element on the canvas, the first one who receives the event is the canvas and after that is sent to the element. This is good because there is no need of another event on element click. So when the canvas receives this event, it deselects all elements. If one element also receives it, then it selects itself.
Converters
This solutions uses a few converters so they need to be explained before going deeper in the code. The two converters below are used on visibility binding of the resize handles. For a better understanding, the resize handles will be numbered from 1
to 8
.
1 2 3
8 4
7 6 5
They also have names related to their position.
1 = TopLeft
2 = Top
3 = TopRight
...
8 = Left
VisibilityConverter
This is a one way converter that converts a boolean value to a visibility specificator like this: true
= Visible and false
= Collapsed.
class VisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
bool b = (bool)value;
return b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
MultiVisibilityConverter
This is a one way multiconverter that converts a boolean value to a visibility specificator, just like the one above, but also takes into account other values. It is needed because you cannot bind the "ConvertParameter
" field of a converter to something. So this is like a converter with more parameters that are binded to something. It takes two or three values: the first one is the visibility value that can be true
or false
and the other two specify if the control can resize vertically and/or horizontally.
For example, resize handle 1 will be displayed only if the element can resize both horizontally and vertically. Let's say that the first parameter is true
- this means that we should display the handle - and the other two parameters are binded on two properties called CanHResize
and CanVResize
which return true
and false
. Theoretically, we should display the handle because the first parameter is true
but when we check the other two, we see that the element cannot resize vertically so handle 1 is not needed.
class MultiVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if ((bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata
(typeof(DependencyObject)).DefaultValue))
return Visibility.Visible;
bool b1 = (bool)values[0];
bool b2 = false;
if (values.Length >= 2)
b2 = (bool)values[1];
if (values.Length == 3)
b2 = b2 && (bool)values[2];
if (!b2)
return Visibility.Collapsed;
return b1 ? Visibility.Visible : Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The list below shows what parameters (only the second and third) are needed for each handle:
1 - CanHResize | CanVResize
2 - CanVResize | -
3 - CanHResize | CanVResize
4 - CanHResize | -
5 - CanHResize | CanVResize
6 - CanVResize | -
7 - CanHResize | CanVResize
8 - CanHResize | -
DesignerComponent
This component wraps any element from the canvas. It provides support for resize and movement of elements.
The XAML part provides the aspect. There are three styles SelectionThumbStyle
, code RoundThumbStyle
and SquareThumbStyle
and nine thumbs (eight for resize handles and one for movement).
SelectionThumbStyle
This style defines the aspect of the selection thumb. It is the blue border that surrounds the selected element.
<Style x:Key="SelectionThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border BorderBrush="#FF385D8A" Background="#00000000"
BorderThickness="2,2,2,2"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
RoundThumbStyle
This style defines the aspect of the round resize thumbs (the ones from corners). It is a round border with stroke and gradient fill.
<Style x:Key="RoundThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4"
BorderBrush="#FF7D8289">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF6FFFF" Offset="0"/>
<GradientStop Color="#FFCAEAED" Offset="0.45"/>
<GradientStop Color="#FFCAEAED" Offset="0.5"/>
<GradientStop Color="#FFCAEAED" Offset="0.55"/>
<GradientStop Color="#FFF6FFFF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
SquareThumbStyle
Same as RoundThumbStyle
just for the square buttons.
<Style x:Key="SquareThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border BorderThickness="1,1,1,1" CornerRadius="0,0,0,0"
BorderBrush="#FF7D8289">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF6FFFF" Offset="0"/>
<GradientStop Color="#FFCAEAED" Offset="0.45"/>
<GradientStop Color="#FFCAEAED" Offset="0.5"/>
<GradientStop Color="#FFCAEAED" Offset="0.55"/>
<GradientStop Color="#FFF6FFFF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Thumbs
There are nine thumbs. Eight for resize, one for every corner and side, and one for movement - as big as the inner control.
The SelectionThumb
is the thumb responsible for movement. The movement offset is provided by the DeltaDrag
event. It's Visibility
property is binded on the IsSelected
property from the user control that contains the thumb - that's why the ancestor is searched. Also the visibility converter is used for the return value of the property.
<Thumb x:Name="SelectionThumb"
Style="{DynamicResource SelectionThumbStyle}"
DragDelta="SelectionThumb_DragDelta"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Cursor="SizeAll"
Visibility="{Binding Path=IsSelected, Converter={StaticResource visibilityConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
The resize handle thumbs are basically the same. Even though similar, they have a major difference: the visibility binding. As previously explained, we pass more arguments to the converter. Because this is handle number 1 (TopLeft
), it takes three arguments, values from properties in the ancestor UserControl
.
<Thumb x:Name="TopLeftThumb"
Style="{DynamicResource RoundThumbStyle}"
DragDelta="Thumb_DragDelta"
HorizontalAlignment="Left" VerticalAlignment="Top"
Width="10" Height="10" Margin="-5,-5,0,0"
Cursor="SizeNWSE" >
<Thumb.Visibility>
<MultiBinding Converter="{StaticResource multiVisibilityConverter}">
<Binding Path="IsSelected"
RelativeSource="{RelativeSource AncestorType={x:Type UserControl}}" />
<Binding Path="CanHResize"
RelativeSource="{RelativeSource AncestorType={x:Type UserControl}}" />
<Binding Path="CanVResize"
RelativeSource="{RelativeSource AncestorType={x:Type UserControl}}" />
</MultiBinding>
</Thumb.Visibility>
</Thumb>
The Code Behind - DesignerComponent.xaml.cs
There is one dependency property, IsSelected
. This needs to be dependency property in order to reflect changes on value change.
private static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool),
typeof(DesignerComponent), new FrameworkPropertyMetadata(false));
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
The minHeight
, minWidth
, maxHeight
and maxWidth
keep the minimum/maximum values of the control. If not specified by the content, they are set to 5.0
for minimum and double.PositiveInfinity
. The 5.0
value is necessary because on less than 5 pixels, the control is unusable. The two properties, CanVResize
and CanHResize
, tell whether the control can resize on a particular axis.
private readonly double minHeight;
private readonly double minWidth;
private readonly double maxHeight;
private readonly double maxWidth;
public bool CanVResize { get; private set; }
public bool CanHResize { get; private set; }
The constructor is just one. It takes as only parameter the FrameworkElement
that will be displayed on canvas. Before setting the content, the FrameworkElement
's properties are checked to determine if it can resize and/or has a minimum/maximum. Also the position on canvas is determined.
Size limit
public DesignerComponent(FrameworkElement content)
{
this.InitializeComponent();
if (!double.IsNaN(content.Width))
{
CanHResize = false;
this.Width = content.Width;
}
else
{
CanHResize = true;
this.Width = 23.0;
}
if (!double.IsNaN(content.Height))
{
CanVResize = false;
this.Height = content.Height; ;
}
else
{
CanVResize = true;
this.Height = 23.0;
}
minWidth = content.MinWidth < 10.0 ? 10.0 : content.MinWidth;
minHeight = content.MinHeight < 10.0 ? 10.0 : content.MinHeight;
maxWidth = content.MaxWidth;
maxHeight = content.MaxHeight;
double top = (double)content.GetValue(Canvas.TopProperty);
if (double.IsNaN(top))
top = 0.0;
double left = (double)content.GetValue(Canvas.LeftProperty);
if (double.IsNaN(left))
left = 0.0;
SetValue(Canvas.TopProperty, top);
SetValue(Canvas.LeftProperty, left);
//Set the actual content. Note that "Content" property is a new property. See below
this.Content = content;
}
The original Content
property is hidden behind a new one. This is because we need to expose the content of the ContentPresenter
not the one belonging to the UserControl
.
public new object Content
{
get
{
return this.ContentComponent.Content;
}
protected set
{
this.ContentComponent.Content = value;
}
}
The DragDelta
event of the SelectionThumb
is handled by SelectionThumb_DragDelta
method. Basically it moves the UserControl
on canvas with the offset specified by the DragDeltaEventArgs
argument.
private void SelectionThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
SetValue(Canvas.LeftProperty, (double)GetValue(Canvas.LeftProperty) + e.HorizontalChange);
SetValue(Canvas.TopProperty, (double)GetValue(Canvas.TopProperty) + e.VerticalChange);
}
The DragDelta
event of the resize thumbs is handled by Thumb_DragDelta
. Based on the name of the thumb, a specified operation is performed.
private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
string name = ((Thumb)sender).Name;
if (name.Contains("Top"))
{
double newHeight = this.Height - e.VerticalChange;
if (newHeight >= minHeight && newHeight <= maxHeight)
{
this.Height = newHeight;
SetValue(Canvas.TopProperty,
(double)GetValue(Canvas.TopProperty) + e.VerticalChange);
}
}
if (name.Contains("Right"))
{
double newWidth = this.Width + e.HorizontalChange;
if (newWidth >= minWidth && newWidth <= maxWidth)
this.Width = newWidth;
}
if (name.Contains("Bottom"))
{
double newHeight = this.Height + e.VerticalChange;
if (newHeight >= minHeight && newHeight <= maxHeight)
this.Height = newHeight;
}
if (name.Contains("Left"))
{
double newWidth = this.Width - e.HorizontalChange;
if (newWidth >= minWidth && newWidth <= maxWidth)
{
this.Width = newWidth;
SetValue(Canvas.LeftProperty,
(double)GetValue(Canvas.LeftProperty) + e.HorizontalChange);
}
}
}
In a previous section called "Tunelling or bubbleing?" I wrote about tunelling. In practice, it is used on the PreviewMouseDown
event of DesignerComponent
.
private void DesignerComponent_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
this.IsSelected = true;
}
Designer
Designer is just an UserControl
with a canvas on it. The XAML part doesn't need too much attention, just notice the PreviewMouseDown
event.
<UserControl x:Class="WPFDesigner.Designer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto">
<Border BorderBrush="#FF000000" BorderThickness="1,1,1,1" x:Name="OuterBorder">
<Canvas ClipToBounds="True" PreviewMouseDown="DesignArea_PreviewMouseDown"
x:Name="DesignArea" Background="#FFFFFFFF"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</Border>
</UserControl>
The interesting part here is the code behind. The Designer
acts like a collection and implements the ICollection
interface. Because this collection is of FrameworkElement
type, the user is not aware that the element goes in a container (DesignerComponenrt
) which is then placed on canvas.
Because another List
of FrameworkElement
is kept internally, it is easy to create a mapping between a control on the canvas and one in this list. When a control is added in Designer, it is actually added in two lists.
protected List<FrameworkElement> frameworkElements = new List<FrameworkElement>();
public void Add(FrameworkElement item)
{
this.frameworkElements.Add(item);
this.DesignArea.Children.Add(new DesignerComponent(item));
}
The same thing happens for all methods from the ICollection
interface.
Using the Code
Using the code is extremely simple. All you have to do is create a new instance of WPFDesigner.Designer
and add it to a form. Afterwards, you can add control in it.
WPFDesigner.Designer designer = new WPFDesigner.Designer();
public Window1()
{
InitializeComponent();
designer.Width = 300;
designer.Height = 300;
MyGrid.Children.Add(designer);
Button b = new Button();
b.SetValue(Canvas.TopProperty, 200.0);
b.MaxHeight = 100.0;
b.Content = "ABCDE";
designer.Add(b);
}
Remarks
If you add a control on WPFDesigner
that has fixed height, the vertical resize handles will not be visible. The same thing is valid for width, but you will not see the horizontal resize handles.
Setting the minimum/maximum width/height on a control and adding it to WPFDesigner
will make the control resize only in that interval.