您的位置:首页 > 其它

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

2010-09-16 19:57 507 查看
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





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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: