WPF Diagramming. Drawing a connection line between two elements with mouse.

http://dvuyka.spaces.live.com/blog/cns!305B02907E9BE19A!171.entry

 

This post will be tightly connected to "WPF. Draggable objects and simple shape connectors " article I've posted earlier so you should reference it for a better understanding of changes made.

I've decided to start a series of articles prefixed with "WPF Diagramming " header. Each time I'll be implementing more complex stuff and fresh ideas to get some king of business process diagramming tool at the end. I'll try to keep all the samples as simple as possible including some additional information towards the WPF programming. Hope that all those who liked the article mentioned will enjoy the new series too.

Note: Until the VS 2008 release all my WPF samples will be prepared using VS 2008 TS beta 2.

Storing control templates in resource dictionaries

As you come across with control templating you may want to collect all your templates separately in one place thus getting access to it from any part of the application. Resource Dictionary is exactly what you will need in this case.

1. Right click your project item in the solution explorer and add a new folder for storing your resources. In my sample I called it "Templates".

2. Right click you "Templates" folder and choose "Add - Resource Dictionary". I called the dictionary "BasicShape.xaml"

Now you are ready to setup your template collection. I moved here my only control template used for Thumb appearance.

<
ResourceDictionary
 xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml">

< ControlTemplate x : Key ="BasicShape1">
< StackPanel >
< Image Name ="tplImage" Source ="/Images/user1.png" Stretch ="Uniform" Width ="32" Height ="32" HorizontalAlignment ="Center"/>
< TextBlock Name ="tplTextBlock" Text ="User stage" HorizontalAlignment ="Center"/>
</ StackPanel >
</ ControlTemplate >

</ ResourceDictionary >

The template is called "BasicShape1" and it still contains the default settings for image and text elements.

 

Attaching resource dictionaries to your application

You've already created your first resource dictionary as "Templates/BasicShape.xaml" and configured your first thumb template. This is a separate resource dictionary and it should be also included to the application to be accessed and used.

Open your application xaml (in my sample it is the most common "App.xaml " file) and define application resources like the following

<
Application
 x
:
Class
="ShapeConnectors.App"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri ="Window1.xaml">

< Application.Resources >
< ResourceDictionary Source ="Templates/BasicShape.xaml"/>
</ Application.Resources >

</
Application >

That's all for the basic resources declaration. From now you will be able to access your "BasicShape1" template like this

Application
.Current.Resources["BasicShape1"
]

Of course you can define more than one resource dictionary but the access path will be as mentioned. That's a perfect stuff I think. To get more information on resource dictionaries and merged resources usage you should really reference the MSDN library.

 

Optimizing geometry on the canvas

For the previous article on shape connectors I used a separate Path class placed to canvas for each LineGeometry object. According to this dummy approach I had to handle the layering stuff. As both start and end points of each line geometry were attached to the center of the opposite thumbs, Thumb objects had to be placed on the top layer (each time ZIndex was set to 1). I decided to eliminate this complexity and remove a lot of useless code by using the native WPF layout features.

Path class can receive a lot of different stuff if you dig MSDN a bit. For the optimization purposes I used exactly GeometryGroup. It is a collection of Geometry, so in our case it can simply be the collection LineGeometry. As our thumbs will concentrate on the starting and ending lines assigned to them rather that on paths, we can freely use only one Path object for holding the entire collection of lines placed on the canvas.

In this case we don't have a strong need of creating a Path object at runtime. Declaring it on the window we also get rid of the layout problems I've mentioned above. Each time we will add a UIElement (Thumb class in our case) to the canvas it will be automatically placed above the Path object and so above all the line connectors it is holding. That's perfect isn't it? ;)

So the 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"
xmlns : my ="clr-namespace:ShapeConnectors"
Title ="Window1" Height ="376" Width ="801" Loaded ="Window_Loaded">

< Canvas Name ="myCanvas">
< Path Stroke ="Black" StrokeThickness ="1">
< Path.Data >
< GeometryGroup x : Name ="connectors"/>
</ Path.Data >
</ Path >
< my : MyThumb x : Name ="myThumb1" Title ="User 1" DragDelta ="onDragDelta" Canvas.Left ="270" Canvas.Top ="63.75" Template ="{ StaticResource BasicShape1 }"/>
< my : MyThumb x : Name ="myThumb2" Title ="User 2" DragDelta ="onDragDelta" Canvas.Left ="270" Canvas.Top ="212.5" Template ="{ StaticResource BasicShape1 }"/>
< my : MyThumb x : Name ="myThumb3" Title ="User 3" DragDelta ="onDragDelta" Canvas.Left ="430" Canvas.Top ="212.5" Template ="{ StaticResource BasicShape1 }"/>
< my : MyThumb x : Name ="myThumb4" Title ="User 4" DragDelta ="onDragDelta" Canvas.Left ="430" Canvas.Top ="63.75" Template ="{ StaticResource BasicShape1 }"/>
< Button Canvas.Left ="15" Canvas.Top ="16" Height ="22" Name ="btnNewAction" Width ="75" Click ="btnNewAction_Click"> new action </ Button >
< Button Canvas.Left ="15" Canvas.Top ="47" Height ="23" Name ="btnNewLink" Width ="75" Click ="btnNewLink_Click"> new link </ Button >
</ Canvas >
</
Window >

As you can see, I've named the geometry group "connectors" so that it can be freely used from code without touching the Path object itself. Again I've implemented 4 sample objects predefined. Thumb declaration is extended with custom properties, this will be discussed later on.

We will need two buttons for current functionality implementation. One button will allow us to add new "Action" object to the canvas. Second buttons will allow us to visually link two objects with a line using the mouse.

 

Optimizing our extended Thumb class

At previous article I've extended basic Thumb class to hold the lines information. We used two collections for storing start and end lines to have a possibility of correct updating line positions upon moving the object across canvas. All the linking functionality was placed in the main program and this is the point of optimization and encapsulation.

As for this time our Thumbs can be distinguished only by title and icon image, we extend "MyThumb" class for supporting two new properties "Title" and "ImageSource"

 #region
 Properties
public static readonly DependencyProperty TitleProperty = DependencyProperty .Register("Title" , typeof (string ), typeof (MyThumb ), new UIPropertyMetadata ("" ));
public static readonly DependencyProperty ImageSourceProperty = DependencyProperty .Register("ImageSource" , typeof (string ), typeof (MyThumb ), new UIPropertyMetadata ("" ));

// This property will hanlde the content of the textblock element taken from control template
public string Title
{
get { return (string )GetValue(TitleProperty); }
set { SetValue(TitleProperty, value ); }
}

// This property will handle the content of the image element taken from control template
public string ImageSource
{
get { return (string )GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value ); }
}

public List <LineGeometry > EndLines { get ; private set ; }
public List <LineGeometry > StartLines { get ; private set ; }
#endregion

This is the most common way of implementing dependency properties, for more detailed information you should better refer to MSDN. I used this approach to be able of further binding and declaring the values for the properties in xaml.

As all our Thumbs will refer to the same or nearly the same control template we can extend the template applying logic to setup the template's elements. Here's what we do

// Upon applying template we apply the "Title" and "ImageSource" properties to the template elements.
public override void OnApplyTemplate()
{
base .OnApplyTemplate();

// Access the textblock element of template and assign it if Title property defined
if (this .Title != string .Empty)
{
TextBlock txt = this .Template.FindName("tplTextBlock" , this ) as TextBlock ;
if (txt != null )
txt.Text = Title;
}

// Access the image element of our custom template and assign it if ImageSource property defined
if (this .ImageSource != string .Empty)
{
Image img = this .Template.FindName("tplImage" , this ) as Image ;
if (img != null )
img.Source = new BitmapImage (new Uri (this .ImageSource, UriKind .Relative));
}
}

This is too simple and well commented I think to dwell on it anymore ;)

 

Concentrating on objects being connected

Though the purpose of the article is to show how to draw the connection lines manually using mouse we should be always concentrated on the Thumb objects as the main entities of our future business process. That's why I've implement some additional helper stuff on setting the connection lines based on Thumb object.

For establishing a link we require two Thumbs. We always know the starting Thumb as we began to draw a line from it's position and we define the end line to set the end of the connector. So the proper logic will be: "Link my current object to this one". Speaking C# our Thumb class will have the following

#region
 Linking logic
// This method establishes a link between current thumb and specified thumb.
// Returns a line geometry with updated positions to be processed outside.
public LineGeometry LinkTo(MyThumb target)
{
// Create new line geometry
LineGeometry line = new LineGeometry ();
// Save as starting line for current thumb
this .StartLines.Add(line);
// Save as ending line for target thumb
target.EndLines.Add(line);
// Ensure both tumbs the latest layout
this .UpdateLayout();
target.UpdateLayout();
// Update line position
line.StartPoint = new Point (Canvas .GetLeft(this ) + this .ActualWidth / 2, Canvas .GetTop(this ) + this .ActualHeight / 2);
line.EndPoint = new Point (Canvas .GetLeft(target) + target.ActualWidth / 2, Canvas .GetTop(target) + target.ActualHeight / 2);
// return line for further processing
return line;
}

// This method establishes a link between current thumb and target thumb using a predefined line geometry
// Note: this is commonly to be used for drawing links with mouse when the line object is predefined outside this class
public bool LinkTo(MyThumb target, LineGeometry line)
{
// Save as starting line for current thumb
this .StartLines.Add(line);
// Save as ending line for target thumb
target.EndLines.Add(line);
// Ensure both tumbs the latest layout
this .UpdateLayout();
target.UpdateLayout();
// Update line position
line.StartPoint = new Point (Canvas .GetLeft(this ) + this .ActualWidth / 2, Canvas .GetTop(this ) + this .ActualHeight / 2);
line.EndPoint = new Point (Canvas .GetLeft(target) + target.ActualWidth / 2, Canvas .GetTop(target) + target.ActualHeight / 2);
return true ;
}
#endregion

As you can see, the first method returns a LineGeometry object upon establishing connection. It returns us a line we can further process in any way required.

This is how I've setup the predefined connectors to be displayed for 4 Thumb objects on "Window Load" event

// Setup connections for predefined thumbs            
connectors.Children.Add(myThumb1.LinkTo(myThumb2));
connectors.Children.Add(myThumb2.LinkTo(myThumb3));
connectors.Children.Add(myThumb3.LinkTo(myThumb4));
connectors.Children.Add(myThumb4.LinkTo(myThumb1));

Very easy isn't it? :) From now we are dealing with our Thumb objects having the connection functionality encapsulated.

That's all as for the preparation part to support easy connectors drawing. Complete sources can be found at the end of the article.

 

Drawing connection lines on the canvas manually

As any other mouse drawing support we basically need to handle three mouse states:

1. Mouse Down - define the start point of the line geometry and initialize the drawing procedure

2. Mouse Move - define the end point of the line geometry

3. Mouse Up - define the end point of the line geometry and finalize the drawing procedure

But we don't intend to create a drawing tool we need to connect two definite objects. So we should allow starting to draw a connector line exactly from one Thumb object, and we should allow ending to draw exactly on another Thumb object. In all other ways drawing procedure is prohibited or finished.

As WPF mouse events allow us to quickly get the element that was clicked the implementation becomes rather trivial.

In the previous sample I've implemented the simple "Add new action" mode for placing Thumb objects to the canvas. Let's define another mode "Add new link" for enabling the connector drawing mode. It will consist of two flags

// flag for enabling "New link" mode
bool isAddNewLink = false ;
// flag that indicates that the drawing link with a mouse started
bool isLinkStarted = false ;

We also need to temporary global variables for handling the starting Thumb and a line drawn

// variable to hold the thumb drawing started from
MyThumb linkedThumb;
// Line drawn by the mouse before connection established
LineGeometry link;

To enter the drawing mode we set the "isAddNewLink" value to "true" with the appropriate button click event.

Our "PreviewMouseLeftButtonDown" event handler for the Window is extended to have the following code

// Is adding new link and a thumb object is clicked...
if (isAddNewLink && e.Source.GetType() == typeof (MyThumb ))
{
if (!isLinkStarted)
{
if (link == null || link.EndPoint != link.StartPoint)
{
Point position = e.GetPosition(this );
link = new LineGeometry (position, position);
connectors.Children.Add(link);
isLinkStarted = true ;
linkedThumb = e.Source as MyThumb ;
e.Handled = true ;
}
}
}

At first we check of course whether the clicked element is our Thumb object to start drawing. Then is we haven't already started drawing we do the initial configuring. We initialize our temporary Line Geometry object and current selected thumb. To view the line on the canvas we should immediately add it to our Path object. But it cannot be the final connector because user can release the mouse button anywhere in unpredictable place. So our task will be to accept the coordinates of the line when the mouse button is up on the Thumb or exclude the line geometry object from the path and remove the line from the canvas. That's what the "isLinkStarted" variable is defined for.

This is how I've implemented the "PreviewMouseLeftButtonUp " event handler for the window

 

// Handles the mouse up event applying the new connection link or resetting it
void Window1_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// If "Add link" mode enabled and line drawing started (line placed to canvas)
if (isAddNewLink && isLinkStarted)
{
// declare the linking state
bool linked = false ;
// We released the button on MyThumb object
if (e.Source.GetType() == typeof (MyThumb ))
{
MyThumb targetThumb = e.Source as MyThumb ;
// define the final endpoint of line
link.EndPoint = e.GetPosition(this );
// if any line was drawn (avoid just clicking on the thumb)
if (link.EndPoint != link.StartPoint && linkedThumb != targetThumb)
{
// establish connection
linkedThumb.LinkTo(targetThumb, link);
// set linked state to true
linked = true ;
}
}
// if we didn't manage to approve the linking state
// button is not released on MyThumb object or double-clicking was performed
if (!linked)
{
// remove line from the canvas
connectors.Children.Remove(link);
// clear the link variable
link = null ;
}

// exit link drawing mode
isLinkStarted = isAddNewLink = false ;
// configure GUI
btnNewAction.IsEnabled = btnNewLink.IsEnabled = true ;
Mouse .OverrideCursor = null ;
e.Handled = true ;
}
this .Title = "Links established: " + connectors.Children.Count.ToString();
}

And at last the most simple event handler in my sample is "PreviewMouseMove " event handler for the Window

// Handles the mouse move event when dragging/drawing the new connection link
void Window1_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (isAddNewLink && isLinkStarted)
{
// Set the new link end point to current mouse position
link.EndPoint = e.GetPosition(this );
e.Handled = true ;
}
}

Here we see the line dynamically changing it's position and length upon mouse moves with a pressed left button.

Here's some screens

connectors_1 connectors_2

All connection lines are automatically dragged with the thumbs. Guess you will like that ;)

 

Some of the features I'm going to prepare for the next article :

1. Implement Thumbs that can be connected predefined times to other objects (for example only one connection allowed)

2. Captions or icons for the connectors (placed always at the center of the line)

3. Different templates for Thumbs

4. Eliminate the possibility of multiple connections of two same thumbs

5. Deleting of connectors

and some other stuff

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值