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.
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
That's all for the basic resources declaration. From now you will be able to access your "BasicShape1" template like this
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
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"
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
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
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
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
We also need to temporary global variables for handling the starting Thumb and a line drawn
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
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
And at last the most simple event handler in my sample is "PreviewMouseMove
" event handler for the Window
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
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
相关文章推荐
- Maximum Difference Between Two Elements
- Using LocalConnection to communicate between two Flex applications
- Moving a drawn line with the mouse
- 【MongoDB】The connection between two tables
- Scenario 2: Drawing Two Triangles with Indexing
- direct2d: antialiasing and drawing a line with a 1 pixel stroke
- Network Connection between Two Hosts
- WPF数据库连接错误:The user is not associated with a trusted SQL Server connection.
- Control two computers with one mouse and keyboard
- Maximum difference between two elements
- Delta phase angle between two sinusoidal waves with different frequencies 不同频率下的相角差
- 169.You have two tables with referential integrity enforced between them. You need to insert data to
- Partition a set of numbers into two so that difference between their sum is minimum with equal size
- 【MongoDB】The connection between two tables
- [转]How to insert a row between two rows in an existing excel with HSSF (Apache POI)
- 越线人群计数--Crossing-line Crowd Counting with Two-phase Deep Neural Networks
- Difference between drawing with QPainter and (QGraphicsView + QGraphicsScene)
- 查找 Two_Line_Text_Combobox 的 OnCbnSelchange 实现处
- SSH connection problem with “Host key verification failed…” error
- 转发:WPF Apps With The Model-View-ViewModel Design Pattern