Tuesday, December 4, 2018

Fun with DockPanel

Here's an example project that allows you to explore how DockPanel docking works. It shows you a menu of four different rectangles and allows you to drag and drop them onto a DockPanel. They dock on the side you drop them nearest. You can also toggle the LastChildFill property to see how that effects the docking.

Start by creating a new WPF project called DockPanelExample.


Here's the XAML. Note I use a canvas so I can drag the rectangles from the stack panel to the dock panel. I need to access the children of the stack panel and the dock panel. I chose to do this by storing a reference to the panels during their loaded events.


<Window x:Class="DockPanelExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DockPanelExample"
        mc:Ignorable="d"
        Title="Dock Panel Example"
        ResizeMode="NoResize"
        SizeToContent="WidthAndHeight"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Canvas Loaded="Canvas_Loaded" MouseMove="Canvas_MouseMove" Width="500" Height="350">
        <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <StackPanel Grid.Column="0" Orientation="Vertical" Background="AliceBlue" Loaded="StackPanel_Loaded" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <CheckBox Content="Last Fill?" IsChecked="{Binding IsLastFill}"/>
            </StackPanel>
            <Border Grid.Column="1" BorderBrush="Navy" BorderThickness="1">
                <DockPanel Background="BlanchedAlmond" Width="400" Height="350" Loaded="DockPanel_Loaded" LastChildFill="{Binding IsLastFill}"></DockPanel>
            </Border>
        </Grid>
    </Canvas>
</Window>

If we create the empty event handlers needed to compile we see a preview of the application. It's not very impressive.


Let's start by adding the code to populate the menu on the left. Start by adding these using statements.

using System;

using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;


Now we should implement INotifyPropertyChanged

public partial class MainWindow : Window, INotifyPropertyChanged

and

        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

Let's modify StackPanel_Loaded to capture the reference to the stack panel and initialize it. Add a place to store the reference.

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private StackPanel Stack;

And then the StackPanel_Loaded event handler.

        private void StackPanel_Loaded(object sender, RoutedEventArgs e)
        {
            Stack = (StackPanel)sender;
            InitializeRectangles();
            InitializeStack();
        }

The routines InitializeRectangles and InitializeStack need a class and a list of that class.

        private class cRectangle
        {
            public String Name;
            public double Width;
            public double Height;
            public Brush Color;
        }

        private List<cRectangle> Rectangles = new List<cRectangle>();

        public void InitializeRectangles()
        {
            Rectangles.Add(new cRectangle { Name = "SmallSquare", Width = 50, Height = 50, Color = new SolidColorBrush(Colors.Red) });
            Rectangles.Add(new cRectangle { Name = "TallRectangle", Width = 50, Height = 100, Color = new SolidColorBrush(Colors.Orange) });
            Rectangles.Add(new cRectangle { Name = "WideRectangle", Width = 100, Height = 50, Color = new SolidColorBrush(Colors.Yellow) });
            Rectangles.Add(new cRectangle { Name = "LargeSquare", Width = 100, Height = 100, Color = new SolidColorBrush(Colors.Green) });
        }

        public void InitializeStack()
        {
            foreach (cRectangle r in Rectangles)
            {
                Stack.Children.Add(InitializeRect(r));
            }
        }

         private Rectangle InitializeRect(cRectangle r)
        {
            Rectangle re = new Rectangle { Tag = r.Name, Width = r.Width, Height = r.Height, Fill = r.Color, Margin = new Thickness(2) };
            return re;
        }

At this point we can run the project and see the initial screen.


We need to be able to click and drag a rectangle onto the dock panel. Let's start with detecting the click event and creating a dragable rectangle. We need a global rectangle object called TempRect. It has a border and is hidden. We will set the other properties in the rectangle's mouse down event.

private Rectangle TempRect = new Rectangle { Stroke = new SolidColorBrush(Colors.Black), StrokeThickness = 1, Visibility = Visibility.Hidden };

Before we proceed we need to capture a reference to the Canvas.

        private Canvas Canvas;

        private void Canvas_Loaded(object sender, RoutedEventArgs e)
        {
            Canvas = (Canvas)sender;
            Canvas.Children.Add(TempRect);
        }

In InitializeRect assign a mouse down handler.

private Rectangle InitializeRect(cRectangle r)
{
    Rectangle re = new Rectangle { Tag = r.Name, Width = r.Width, Height = r.Height, Fill = r.Color, Margin = new Thickness(2) };
    re.MouseLeftButtonDown += DoMouseDown;
    return re;
}

private void DoMouseDown(object sender, MouseEventArgs e)
{
    Rectangle SourceRect = (Rectangle)sender;
    Point SourceLocation;

    CloneRect(SourceRect);
    TempRect.Visibility = Visibility.Visible;
    SourceLocation = SourceRect.TranslatePoint(new Point(0, 0), Canvas);
    TempRect.SetValue(Canvas.TopProperty, SourceLocation.Y);
    TempRect.SetValue(Canvas.LeftProperty, SourceLocation.X);
}

private void CloneRect(Rectangle r)
{
    TempRect.Tag = r.Tag;
    TempRect.Width = r.Width;
    TempRect.Height = r.Height;
    TempRect.Fill = r.Fill;
}

At this point clicking on one of the rectangles will clone the rectangle with a border. Now we need to figure out how to drag it. We start by determining how far the mouse cursor is from the top/left corner of the rectangle, then every time the mouse moves, the rectangle follows it.

Add two new doubles to the class.

        private Double MouseOffsetX;
        private Double MouseOffsetY;

and initialize them in DoMouseDown.

        MouseOffsetX = SourceLocation.X - e.GetPosition(Canvas).X;
        MouseOffsetY = SourceLocation.Y - e.GetPosition(Canvas).Y;

While we are in DoMouseDown we need to capture the mouse so that even when it is dragged off of the menu rectangle, it's events are still passed to the menu rectangle. Add this to DoMouseDown.

        Mouse.Capture(SourceRect);

Now we complete our Canvas_MouseMove event handler. If the temporary rectangle is visible we make it track mouse movements.

        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {
            if (TempRect.Visibility == Visibility.Visible)
            {
                Point NewPosition = e.GetPosition(Canvas);
                TempRect.SetValue(Canvas.TopProperty, NewPosition.Y + MouseOffsetY);
                TempRect.SetValue(Canvas.LeftProperty, NewPosition.X + MouseOffsetX);
            }
        }


Now you can drag the selected rectangle.


Our next task is to handle the drop. This is done via the MouseUp event. We determine which side of the dock panel the mouse is nearest, create a new rectangle, dock it, and hide the temporary rectangle.

To handle the drop, we need to capture a reference to the DockPanel in it's loaded event. Create a class reference to the DockPanel.

    private DockPanel Dock;

and flesh out the DockPanel_Loaded event handler.

    private void DockPanel_Loaded(object sender, RoutedEventArgs e)
    {
        Dock = (DockPanel)sender;
    }

Let's start by defining and registering the Rectangle's MouseUp event handler. In InitializeRect, register a DoMouseUp event handler.

    re.MouseLeftButtonUp += DoMouseUp;

Most of the event handler is involved with determining how to dock the rectangle. Once we have done that we dock the rectangle, add it to the dock panel, hide the temporary rectange, and turn off mouse capture. It looks like this.

private void DoMouseUp(object sender, MouseEventArgs e)
{
    Rectangle NewRect = new Rectangle { Tag = TempRect.Tag, Width = TempRect.Width, Height = TempRect.Height, Fill = TempRect.Fill };
    Point MousePosition = e.GetPosition(Dock);
    if (MousePosition.X > 0 && MousePosition.X < Dock.Width && MousePosition.Y > 0 && MousePosition.Y < Dock.Height)
    {
        Double DistFromTop = e.GetPosition(Dock).Y;
        Double DistFromBottom = Dock.Height - e.GetPosition(Dock).Y;
        Double DistFromLeft = e.GetPosition(Dock).X;
        Double DistFromRight = Dock.Width - e.GetPosition(Dock).X;
        Double MinDistX = Math.Min(DistFromLeft, DistFromRight);
        Double MinDistY = Math.Min(DistFromTop, DistFromBottom);
        Double MinDist = Math.Min(MinDistX, MinDistY);
        Dock DockLocation = System.Windows.Controls.Dock.Top;
        if (MinDist == DistFromBottom) DockLocation = System.Windows.Controls.Dock.Bottom;
        if (MinDist == DistFromLeft) DockLocation = System.Windows.Controls.Dock.Left;
        if (MinDist == DistFromRight) DockLocation = System.Windows.Controls.Dock.Right;

        NewRect.SetValue(DockPanel.DockProperty, DockLocation);
        Dock.Children.Add(NewRect);
    }
    TempRect.Visibility = Visibility.Hidden;
    Mouse.Capture(null);
}

Now we can click, drag, and drop a rectangle into the DockPanel.


Our next task is to wire up the Last Fill checkbox by binding it to a property and binding the DockPanel's LastChildFill property to the same property. Our XAML already references this property so let's create it.

        private bool _IsLastFill;
        public bool IsLastFill
        {
            get { return _IsLastFill; }
            set { _IsLastFill = value;
                NotifyPropertyChanged("IsLastFill");            }
        }

 Now checking the Last Fill check box allows you to see the effect of setting the LastChildFill property of the Dock Panel.


Our last task is to allow the user to click on a rectangle in the Dock Panel to delete it. In DoMouseDown add a line to register a ButtonDown event handler on the new rectangle.

    NewRect.MouseLeftButtonDown += DeleteRect;

The event handler looks like this.

    private void DeleteRect(object sender, MouseEventArgs e)
    {
        Rectangle DeletedRect = (Rectangle)sender;
        Dock.Children.Remove(DeletedRect);
        DeletedRect.MouseLeftButtonDown -= DeleteRect;
        DeletedRect = null;
    }

The full code behind looks like this...
-----------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace DockPanelExample
{

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private StackPanel Stack;
        private DockPanel Dock;
        private Canvas Canvas;
        private Double MouseOffsetX;
        private Double MouseOffsetY;
        private Rectangle TempRect = new Rectangle { Stroke = new SolidColorBrush(Colors.Black), StrokeThickness = 1, Visibility = Visibility.Hidden };

        private class cRectangle
        {
            public String Name;
            public double Width;
            public double Height;
            public Brush Color;
        }

        private List<cRectangle> Rectangles = new List<cRectangle>();

        private bool _IsLastFill;
        public bool IsLastFill
        {
            get { return _IsLastFill; }
            set { _IsLastFill = value;
                NotifyPropertyChanged("IsLastFill");            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        public void InitializeRectangles()
        {
            Rectangles.Add(new cRectangle { Name = "SmallSquare", Width = 50, Height = 50, Color = new SolidColorBrush(Colors.Red) });
            Rectangles.Add(new cRectangle { Name = "TallRectangle", Width = 50, Height = 100, Color = new SolidColorBrush(Colors.Orange) });
            Rectangles.Add(new cRectangle { Name = "WideRectangle", Width = 100, Height = 50, Color = new SolidColorBrush(Colors.Yellow) });
            Rectangles.Add(new cRectangle { Name = "LargeSquare", Width = 100, Height = 100, Color = new SolidColorBrush(Colors.Green) });
        }

        public void InitializeStack()
        {
            foreach (cRectangle r in Rectangles)
            {
                Stack.Children.Add(InitializeRect(r));
            }
        }

        private Rectangle InitializeRect(cRectangle r)
        {
            Rectangle re = new Rectangle { Tag = r.Name, Width = r.Width, Height = r.Height, Fill = r.Color, Margin = new Thickness(2) };
            re.MouseLeftButtonDown += DoMouseDown;
            re.MouseLeftButtonUp += DoMouseUp;
            return re;
        }

        private void CloneRect(Rectangle r)
        {
            TempRect.Tag = r.Tag;
            TempRect.Width = r.Width;
            TempRect.Height = r.Height;
            TempRect.Fill = r.Fill;
        }

        private void DoMouseDown(object sender, MouseEventArgs e)
        {
            Rectangle SourceRect = (Rectangle)sender;
            Point SourceLocation;

            CloneRect(SourceRect);
            TempRect.Visibility = Visibility.Visible;
            SourceLocation = SourceRect.TranslatePoint(new Point(0, 0), Canvas);
            TempRect.SetValue(Canvas.TopProperty, SourceLocation.Y);
            TempRect.SetValue(Canvas.LeftProperty, SourceLocation.X);
            MouseOffsetX = SourceLocation.X - e.GetPosition(Canvas).X;
            MouseOffsetY = SourceLocation.Y - e.GetPosition(Canvas).Y;
            Mouse.Capture(SourceRect);
        }

        private void DoMouseUp(object sender, MouseEventArgs e)
        {
            Rectangle NewRect = new Rectangle { Tag = TempRect.Tag, Width = TempRect.Width, Height = TempRect.Height, Fill = TempRect.Fill };
            Point MousePosition = e.GetPosition(Dock);
            if (MousePosition.X > 0 && MousePosition.X < Dock.Width && MousePosition.Y > 0 && MousePosition.Y < Dock.Height)
            {
                Double DistFromTop = e.GetPosition(Dock).Y;
                Double DistFromBottom = Dock.Height - e.GetPosition(Dock).Y;
                Double DistFromLeft = e.GetPosition(Dock).X;
                Double DistFromRight = Dock.Width - e.GetPosition(Dock).X;
                Double MinDistX = Math.Min(DistFromLeft, DistFromRight);
                Double MinDistY = Math.Min(DistFromTop, DistFromBottom);
                Double MinDist = Math.Min(MinDistX, MinDistY);
                Dock DockLocation = System.Windows.Controls.Dock.Top;
                if (MinDist == DistFromBottom) DockLocation = System.Windows.Controls.Dock.Bottom;
                if (MinDist == DistFromLeft) DockLocation = System.Windows.Controls.Dock.Left;
                if (MinDist == DistFromRight) DockLocation = System.Windows.Controls.Dock.Right;

                NewRect.SetValue(DockPanel.DockProperty, DockLocation);
                NewRect.MouseLeftButtonDown += DeleteRect;
                Dock.Children.Add(NewRect);
            }
            TempRect.Visibility = Visibility.Hidden;
            Mouse.Capture(null);
        }

        private void DeleteRect(object sender, MouseEventArgs e)
        {
            Rectangle DeletedRect = (Rectangle)sender;
            Dock.Children.Remove(DeletedRect);
            DeletedRect.MouseLeftButtonDown -= DeleteRect;
            DeletedRect = null;
        }

        private void StackPanel_Loaded(object sender, RoutedEventArgs e)
        {
            Stack = (StackPanel)sender;
            InitializeRectangles();
            InitializeStack();
        }

        private void DockPanel_Loaded(object sender, RoutedEventArgs e)
        {
            Dock = (DockPanel)sender;
        }

        private void Canvas_Loaded(object sender, RoutedEventArgs e)
        {
            Canvas = (Canvas)sender;
            Canvas.Children.Add(TempRect);
        }

        private void Canvas_MouseMove(object sender, MouseEventArgs e)
        {
            if (TempRect.Visibility == Visibility.Visible)
            {
                Point NewPosition = e.GetPosition(Canvas);
                TempRect.SetValue(Canvas.TopProperty, NewPosition.Y + MouseOffsetY);
                TempRect.SetValue(Canvas.LeftProperty, NewPosition.X + MouseOffsetX);
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
    }
}

No comments:

Post a Comment