使用wpf轻松创建可拖拉的图形,形状或控件,并且在这些对象之间用线条相连,在拖动对象时线条也跟随移动。
就像visio, powerpoint中一样的
转自 from: http://dvuyka.spaces.live.com/Blog/cns!305B02907E9BE19A!155.entry
WPF. Draggable objects and simple shape connectors
Last weekend I found very good sample of using Thumb class for implementing draggable objects. In two words as all other elements in WPF the Thumb also can be templated. And nothing stops you to get all drag functionality of Thumb turning it to anything you need. That's perfect I think ;)
So how to easily create a simple draggable object based on a Thumb?
< Thumb Name ="myThumb" DragDelta ="onDragDelta" Canvas.Left ="0" Canvas.Top ="0" Template ="{ StaticResource template1 }"/>
Here we declare a Thumb, set the "onDragDelta" handler for the "DragDelta" event and assign to it custom template called "template1".
After that we create the most simpliest shape template that can be found everywhere in the internet
< ControlTemplate x : Key ="template1">
< Ellipse Width ="60" Height ="30" Fill ="Black"/>
</ ControlTemplate >
As you can see it turns our thumb to a dummy black shape of (60;30) size.
The complete xaml for the window will be as following
< Window x : Class ="ShapeConnectors.Window1"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
Title ="DragableObject" Height ="300" Width ="300"
>
< Window.Resources >
< ResourceDictionary >
< ControlTemplate x : Key ="template1">
< Ellipse Width ="60" Height ="30" Fill ="Black"/>
</ ControlTemplate >
</ ResourceDictionary >
</ Window.Resources >
< Canvas Name ="myCanvas">
< Thumb Name ="myThumb" DragDelta ="onDragDelta" Canvas.Left ="0" Canvas.Top ="0" Template ="{ StaticResource template1 }"/>
</ Canvas >
</ Window >
As our dummy black shape is still a Thumb element it needs actually one event hanler for the basic drag support implementation - "onDragDelta". Implementing code behind is this way is too boring...
void onDragDelta(object sender, DragDeltaEventArgs e)
{
Canvas .SetLeft(myThumb, Canvas .GetLeft(myThumb) + e.HorizontalChange);
Canvas .SetTop(myThumb, Canvas .GetTop(myThumb) + e.VerticalChange);
}
We get the original position of the element being dragged and add the new offset values.
After playing a couple of minutes with the sample above I decided to complicate it a bit to get something more intresting. I wanted to create some workflow-like draggable objects connected to each other with simple shape connectors using basic line geometry. Upon moving the shapes across the canvas line connectors should followed the objects too. Additionally I wanted to have possibility of adding new shapes by clicking at any place of the canvas with establishing any simple line connector to the existing object.
Something like this
As far as we get the task to play, what will be the most simple concept of getting the desired result?
Each shape can possibly be connected to any number of another shapes. For hanling the position of each connector while dragging the object we need to somehow control the start and end points of the line element connected to two shapes. So it becomes obvious that each shape should contain the list of line's start and end points separately so that line positioning and length can be easily updated by the shape itself or outter code.
Let's inherit the basic Thumb class providing the required functionality
MyThumb.cs
using System.Collections.Generic;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace ShapeConnectors
{
public class MyThumb : Thumb
{
public List <LineGeometry > EndLines { get ; private set ; }
public List <LineGeometry > StartLines { get ; private set ; }
public MyThumb() : base ()
{
StartLines = new List <LineGeometry >();
EndLines = new List <LineGeometry >();
}
}
}
It's very easy now to change the xaml part to use our extended Thumb element
< Window x : Class ="ShapeConnectors.Window1"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : my ="clr-namespace:ShapeConnectors"
Title ="Window1" Height ="376.25" Width ="801.25" Loaded ="Window_Loaded">
< Canvas Name ="myCanvas">
< my : MyThumb x : Name ="myThumb1" DragDelta ="onDragDelta" Canvas.Left ="270" Canvas.Top ="63.75" Template ="{ StaticResource template1 }"/>
</ Canvas >
</ Window >
Here we define our hamespace "ShapeConnectors" and prefix "my".
Note that should name the element by using "x:Name" syntax because we put the existing though inherited element to xaml.
As can be seen from the screenshots, our shape should contain an icon and a name elements. Let's change thumb's template to get it working.
< ControlTemplate x : Key ="template1">
< StackPanel >
< Image Name ="tplImage" Source ="Images/user1.png" Stretch ="Uniform" Width ="32" Height ="32"/>
< TextBlock Name ="tplTextBlock" Text ="User stage"/>
</ StackPanel >
</ ControlTemplate >
We provide a default template for all the draggable objects. Each object contains an image element referencing "Images/user1.png" picture from the resources and contains a text block "User stage" (you can change it to anything you want). Later we will access this template directly from the code, so it is important to name the elements.
Full xaml for our window will be the following
< Window x : Class ="ShapeConnectors.Window1"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : my ="clr-namespace:ShapeConnectors"
Title ="Window1" Height ="376" Width ="801" Loaded ="Window_Loaded">
< Window.Resources >
< ResourceDictionary >
< ControlTemplate x : Key ="template1">
< StackPanel >
< Image Name ="tplImage" Source ="Images/user1.png" Stretch ="Uniform" Width ="32" Height ="32"/>
< TextBlock Name ="tplTextBlock" Text ="User stage"/>
</ StackPanel >
</ ControlTemplate >
</ ResourceDictionary >
</ Window.Resources >
< Canvas Name ="myCanvas">
< my : MyThumb x : Name ="myThumb1" DragDelta ="onDragDelta" Canvas.Left ="270" Canvas.Top ="63.75" Template ="{ StaticResource template1 }"/>
< my : MyThumb x : Name ="myThumb2" DragDelta ="onDragDelta" Canvas.Left ="270" Canvas.Top ="212.5" Template ="{ StaticResource template1 }"/>
< my : MyThumb x : Name ="myThumb3" DragDelta ="onDragDelta" Canvas.Left ="430" Canvas.Top ="212.5" Template ="{ StaticResource template1 }"/>
< my : MyThumb x : Name ="myThumb4" DragDelta ="onDragDelta" Canvas.Left ="430" Canvas.Top ="63.75" Template ="{ StaticResource template1 }"/>
< Button Canvas.Left ="15" Canvas.Top ="16" Height ="22" Name ="btnNewAction" Width ="75" Click ="btnNewAction_Click"> new action </ Button >
</ Canvas >
</ Window >
I've added four thumbs by default. Additionally I've created a button called "btnNewAction" that will be enabling the mode of adding new objects by clicking somewhere on the canvas. One button click - one thumb to be created anywhere on the canvas and linked to the predefined "myThumb2" element.
As for line geometry we'll be using the Path element. Each path element will be hosting one line.
So here's going the main part of our application
Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace ShapeConnectors
{
public partial class Window1 : Window
{
// simple flag for enabling "New thumb" mode
bool isAddNew = false ;
// Paths for our predefined thumbs
Path path1;
Path path2;
Path path3;
Path path4;
public Window1()
{
InitializeComponent();
}
// Event hanlder for dragging functionality support same to all thumbs
private void onDragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
MyThumb thumb = e.Source as MyThumb ;
double left = Canvas .GetLeft(thumb) + e.HorizontalChange;
double top = Canvas .GetTop(thumb) + e.VerticalChange;
Canvas .SetLeft(thumb, left);
Canvas .SetTop(thumb, top);
// Update lines's layouts
UpdateLines(thumb);
}
// This method updates all the starting and ending lines assigned for the given thumb
// according to the latest known thumb position on the canvas
private void UpdateLines(MyThumb thumb)
{
double left = Canvas .GetLeft(thumb);
double top = Canvas .GetTop(thumb);
for (int i = 0; i < thumb.StartLines.Count; i++)
thumb.StartLines[i].StartPoint = new Point (left + thumb.ActualWidth / 2, top + thumb.ActualHeight / 2);
for (int i = 0; i < thumb.EndLines.Count; i++)
thumb.EndLines[i].EndPoint = new Point (left + thumb.ActualWidth / 2, top + thumb.ActualHeight / 2);
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Move all the predefined thumbs to the front to be over the lines
Canvas .SetZIndex(myThumb1, 1);
Canvas .SetZIndex(myThumb2, 1);
Canvas .SetZIndex(myThumb3, 1);
Canvas .SetZIndex(myThumb4, 1);
#region Initialize paths for predefined thumbs
path1 = new Path ();
path1.Stroke = Brushes .Black;
path1.StrokeThickness = 1;
path2 = new Path ();
path2.Stroke = Brushes .Blue;
path2.StrokeThickness = 1;
path3 = new Path ();
path3.Stroke = Brushes .Green;
path3.StrokeThickness = 1;
path4 = new Path ();
path4.Stroke = Brushes .Red;
path4.StrokeThickness = 1;
myCanvas.Children.Add(path1);
myCanvas.Children.Add(path2);
myCanvas.Children.Add(path3);
myCanvas.Children.Add(path4);
#endregion
#region Initialize line geometry for predefined thumbs
LineGeometry line1 = new LineGeometry ();
path1.Data = line1;
LineGeometry line2 = new LineGeometry ();
path2.Data = line2;
LineGeometry line3 = new LineGeometry ();
path3.Data = line3;
LineGeometry line4 = new LineGeometry ();
path4.Data = line4;
#endregion
#region Setup connections for predefined thumbs
myThumb1.StartLines.Add(line1);
myThumb2.EndLines.Add(line1);
myThumb2.StartLines.Add(line2);
myThumb3.EndLines.Add(line2);
myThumb3.StartLines.Add(line3);
myThumb4.EndLines.Add(line3);
myThumb4.StartLines.Add(line4);
myThumb1.EndLines.Add(line4);
#endregion
#region Update lines' layouts
UpdateLines(myThumb1);
UpdateLines(myThumb2);
UpdateLines(myThumb3);
UpdateLines(myThumb4);
#endregion
this .PreviewMouseLeftButtonDown += new MouseButtonEventHandler (Window1_PreviewMouseLeftButtonDown);
}
// Event handler for creating new thumb element by left mouse click
// and visually connecting it to the myThumb2 element
void Window1_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (isAddNew)
{
// Create new thumb object
MyThumb newThumb = new MyThumb ();
// Assign our custom template to it
newThumb.Template = this .Resources["template1" ] as ControlTemplate ;
// Calling ApplyTemplate enables us to navigate the visual tree right now (important!)
newThumb.ApplyTemplate();
// Add the "onDragDelta" event handler that is common to all objects
newThumb.DragDelta +=new DragDeltaEventHandler (onDragDelta);
// Put newly created thumb on the canvas
myCanvas.Children.Add(newThumb);
// Access the image element of our custom template and assign it to the new image
Image img = (Image )newThumb.Template.FindName("tplImage" , newThumb);
img.Source = new BitmapImage (new Uri ("Images/gear_connection.png" , UriKind .Relative));
// Access the textblock element of template and change it too
TextBlock txt = (TextBlock )newThumb.Template.FindName("tplTextBlock" , newThumb);
txt.Text = "System action" ;
// Set the position of the object according to the mouse pointer
Point position = e.GetPosition(this );
Canvas .SetLeft(newThumb, position.X);
Canvas .SetTop(newThumb, position.Y);
// Move our thumb to the front to be over the lines
Canvas .SetZIndex(newThumb, 1);
// Manually update the layout of the thumb (important!)
newThumb.UpdateLayout();
// Create new path and put it on the canvas
Path newPath = new Path ();
newPath.Stroke = Brushes .Black;
newPath.StrokeThickness = 1;
myCanvas.Children.Add(newPath);
// Create new line geometry element and assign the path to it
LineGeometry newLine = new LineGeometry ();
newPath.Data = newLine;
// Predefined "myThumb2" element will host the starting point
myThumb2.StartLines.Add(newLine);
// Our new thumb will host the ending point of the line
newThumb.EndLines.Add(newLine);
// Update the layout of line geometry
UpdateLines(newThumb);
UpdateLines(myThumb2);
isAddNew = false ;
Mouse .OverrideCursor = null ;
btnNewAction.IsEnabled = true ;
e.Handled = true ;
}
}
// Event handler for enabling new thumb creation by left mouse button click
private void btnNewAction_Click(object sender, RoutedEventArgs e)
{
isAddNew = true ;
Mouse .OverrideCursor = Cursors .SizeAll;
btnNewAction.IsEnabled = false ;
}
}
}
Here's what we can have upon playing a bit with the applicaition
This sample if too far from the real life application but I tried to keep the code as simple as possible for all to be able to investigate the process and find out own ways of implementing the desired idea.
Have a nice testing and coding.