|
<Window x:Class="SampleCode.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SampleCode"
xmlns:NetworkUI="clr-namespace:NetworkUI;assembly=NetworkUI"
xmlns:NetworkModel="clr-namespace:NetworkModel;assembly=NetworkModel"
xmlns:ZoomAndPan="clr-namespace:ZoomAndPan;assembly=ZoomAndPan"
xmlns:ac="clr-namespace:AdornedControl;assembly=AdornedControl"
xmlns:diagnostics="clr-namespace:System.Diagnostics;assembly=WindowsBase"
Title="Main Window"
x:Name="mainWindow"
MinWidth="400"
MinHeight="150"
Width="800"
Height="500"
Loaded="MainWindow_Loaded"
FocusManager.FocusedElement="{Binding ElementName=networkControl}"
>
<!--
Including this in binding statements for diagnostics:
diagnostics:PresentationTraceSources.TraceLevel=High
-->
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!--
Merge in the resource dictionary that is shared between the main window and the overview window.
-->
<ResourceDictionary
Source="SharedVisualTemplates.xaml"
/>
</ResourceDictionary.MergedDictionaries>
<!-- UI commands. -->
<RoutedUICommand x:Key="Commands.DeleteSelectedNodes" />
<RoutedUICommand x:Key="Commands.CreateNode" />
<RoutedUICommand x:Key="Commands.DeleteNode" />
<RoutedUICommand x:Key="Commands.DeleteConnection" />
<RoutedUICommand x:Key="Commands.ZoomOut" />
<RoutedUICommand x:Key="Commands.ZoomIn" />
<RoutedUICommand x:Key="Commands.JumpBackToPrevZoom" />
<RoutedUICommand x:Key="Commands.FitContent" />
<RoutedUICommand x:Key="Commands.Fill" />
<RoutedUICommand x:Key="Commands.OneHundredPercent" />
<!--
This converts from a scale value to a percentage value.
It is used to convert the value of 'ContentScale' to the percentage zoom level that is displayed in the UI.
-->
<local:ScaleToPercentConverter
x:Key="scaleToPercentConverter"
/>
<!--
This graphic is used to indicate that a connection cannot be made between two particular connectors.
-->
<DataTemplate DataType="{x:Type local:ConnectionBadIndicator}">
<Grid
Width="80"
>
<Image
Width="32"
Height="32"
Source="Resources/block_16.png"
HorizontalAlignment="Right"
/>
</Grid>
</DataTemplate>
<!--
This graphic is used to indicate that a connection can be made between two particular connectors.
-->
<DataTemplate DataType="{x:Type local:ConnectionOkIndicator}">
<Grid
Width="80"
>
<Image
Width="32"
Height="32"
Source="Resources/tick_16.png"
HorizontalAlignment="Right"
/>
</Grid>
</DataTemplate>
<!--
Define the visual style for a 'ConnectorItem'.
-->
<Style
TargetType="{x:Type NetworkUI:ConnectorItem}"
>
<!--
Data-binding for the connector hotspot.
ConnectorItem automatically computes its center points and assings this value
to the 'Hotspot' property. This data-binding then 'pushes' the value into the application
view-model.
-->
<Setter
Property="Hotspot"
Value="{Binding Hotspot, Mode=OneWayToSource}"
/>
<!-- The visual template. -->
<Setter
Property="Template"
>
<Setter.Value>
<ControlTemplate
TargetType="{x:Type NetworkUI:ConnectorItem}"
>
<!-- The visual for the connector. -->
<Ellipse
Stroke="{StaticResource nodeBorderBrush}"
Fill="{StaticResource connectorBackgroundBrush}"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
Template for the button that is used to delete nodes and connections in the graph.
This button is displayed in an adorner when the user hovers the mouse over a node or connection.
-->
<ControlTemplate
x:Key="deleteButtonTemplate"
TargetType="{x:Type Button}"
>
<Grid
x:Name="grid"
>
<Grid.RenderTransform>
<ScaleTransform
ScaleX="1"
ScaleY="1"
CenterX="10"
CenterY="10"
/>
</Grid.RenderTransform>
<Ellipse
x:Name="shadow"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Fill="Gray"
>
<Ellipse.RenderTransform>
<TranslateTransform
X="1.5"
Y="1.5"
/>
</Ellipse.RenderTransform>
</Ellipse>
<Ellipse
x:Name="ellipse"
Stroke="Black"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Fill="White"
/>
<Image
Source="Resources\scissors.png"
Margin="2"
/>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger
RoutedEvent="Mouse.MouseEnter"
>
<!--
Make the 'delete connection button' larger when the mouse
cursor is hovered over it.
-->
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="grid"
Storyboard.TargetProperty="RenderTransform.ScaleX"
To="1.3"
Duration="0:0:0.25"
/>
<DoubleAnimation
Storyboard.TargetName="grid"
Storyboard.TargetProperty="RenderTransform.ScaleY"
To="1.3"
Duration="0:0:0.25"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger
RoutedEvent="Mouse.MouseLeave"
>
<!--
Return the 'delete connection button' to normal size when the mouse
cursor is moved away.
-->
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="grid"
Storyboard.TargetProperty="RenderTransform.ScaleX"
To="1"
Duration="0:0:0.05"
/>
<DoubleAnimation
Storyboard.TargetName="grid"
Storyboard.TargetProperty="RenderTransform.ScaleY"
To="1"
Duration="0:0:0.05"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!--
Data-template for ConectionViewModel.
Note that the 'Start' and 'End' of the arrow are bound to 'SourceConnectorHotspot' and 'DestConnectorHotspot' in
the view-model.
In this sample a curved arrow represents connections between nodes.
-->
<DataTemplate
DataType="{x:Type NetworkModel:ConnectionViewModel}"
>
<!--
An adorned control is used, to represent the connection.
When the user hovers the mouse cursor over the connection, the
'delete connection' adorner pops up and allows them to delete the connection.
-->
<ac:AdornedControl
HorizontalAdornerPlacement="Mouse"
VerticalAdornerPlacement="Mouse"
IsMouseOverShowEnabled="{Binding ElementName=networkControl, Path=IsNotDragging}"
>
<!-- The connection is represented by a curved arrow. -->
<local:CurvedArrow
Stroke="{StaticResource connectionBrush}"
StrokeThickness="2"
Fill="{StaticResource connectionBrush}"
Points="{Binding Points}"
/>
<ac:AdornedControl.AdornerContent>
<!--
This is the adorner that pops up when the user hovers the mouse over the connection.
It displays a button that the user can click to delete the connection.
-->
<Canvas
x:Name="connectionAdornerCanvas"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Width="30"
Height="30"
>
<Line
X1="0"
Y1="30"
X2="15"
Y2="15"
Stroke="Black"
StrokeThickness="1"
/>
<Button
x:Name="deleteConnectionButton"
Canvas.Left="10"
Canvas.Top="0"
Width="20"
Height="20"
Cursor="Hand"
Focusable="False"
Command="{StaticResource Commands.DeleteConnection}"
CommandParameter="{Binding}"
Template="{StaticResource deleteButtonTemplate}"
/>
</Canvas>
</ac:AdornedControl.AdornerContent>
</ac:AdornedControl>
</DataTemplate>
<!-- Define a data-template for the 'NodeViewModel' class. -->
<DataTemplate
DataType="{x:Type NetworkModel:NodeViewModel}"
>
<!--
An adorned control is used, to represent the node.
When the user hovers the mouse cursor over the node, the
'delete node' adorner pops up and allows them to delete the node.
-->
<ac:AdornedControl
HorizontalAdornerPlacement="Outside"
VerticalAdornerPlacement="Outside"
AdornerOffsetX="-12"
AdornerOffsetY="8"
IsMouseOverShowEnabled="{Binding ElementName=networkControl, Path=IsNotDragging}"
>
<!-- The margin has been selected so that the selection rect nicely covers the entire node. -->
<Grid
MinWidth="120"
Margin="10,6,10,6"
SizeChanged="Node_SizeChanged"
>
<!-- This rectangle is the main visual for the node. -->
<Rectangle
Stroke="{StaticResource nodeBorderBrush}"
StrokeThickness="1.3"
RadiusX="4"
RadiusY="4"
Fill="{StaticResource nodeFillBrush}"
/>
<!--
This grid contains the node's connectors.
The margin is negative so that the connectors overlap the body of the node and it's selection border.
-->
<Grid
Margin="-6,4,-6,4"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" MinWidth="10" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<!-- spacer -->
<RowDefinition Height="2" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- The name of the node. -->
<TextBlock
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="0"
Text="{Binding Name}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
<!-- Displays the node's input connectors. -->
<ItemsControl
Grid.Column="0"
Grid.Row="2"
ItemsSource="{Binding InputConnectors}"
ItemTemplate="{StaticResource inputConnectorTemplate}"
Focusable="False"
/>
<!-- Displays the node's output connectors. -->
<ItemsControl
Grid.Column="2"
Grid.Row="2"
ItemsSource="{Binding OutputConnectors}"
ItemTemplate="{StaticResource outputConnectorTemplate}"
Focusable="False"
/>
</Grid>
</Grid>
<ac:AdornedControl.AdornerContent>
<!--
This is the adorner that pops up when the user hovers the mouse over the node.
It displays a button that the user can click to delete the node.
-->
<Canvas
x:Name="nodeAdornerCanvas"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Width="30"
Height="30"
>
<Line
X1="0"
Y1="30"
X2="15"
Y2="15"
Stroke="Black"
StrokeThickness="1"
/>
<Button
x:Name="deleteNodeButton"
Canvas.Left="10"
Canvas.Top="0"
Width="20"
Height="20"
Cursor="Hand"
Focusable="False"
Command="{StaticResource Commands.DeleteNode}"
CommandParameter="{Binding}"
Template="{StaticResource deleteButtonTemplate}"
/>
</Canvas>
</ac:AdornedControl.AdornerContent>
</ac:AdornedControl>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<Window.InputBindings>
<!-- Bind input to commands. -->
<KeyBinding
Key="Del"
Command="{StaticResource Commands.DeleteSelectedNodes}"
/>
<KeyBinding
Key="Minus"
Command="{StaticResource Commands.ZoomOut}"
/>
<KeyBinding
Key="Plus"
Command="{StaticResource Commands.ZoomIn}"
/>
<KeyBinding
Key="Backspace"
Command="{StaticResource Commands.JumpBackToPrevZoom}"
/>
<KeyBinding
Key="Space"
Command="{StaticResource Commands.FitContent}"
/>
</Window.InputBindings>
<Window.CommandBindings>
<!-- Bind commands to event handlers.-->
<CommandBinding
Command="{StaticResource Commands.DeleteSelectedNodes}"
Executed="DeleteSelectedNodes_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.CreateNode}"
Executed="CreateNode_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.DeleteNode}"
Executed="DeleteNode_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.DeleteConnection}"
Executed="DeleteConnection_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.ZoomOut}"
Executed="ZoomOut_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.ZoomIn}"
Executed="ZoomIn_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.JumpBackToPrevZoom}"
Executed="JumpBackToPrevZoom_Executed"
CanExecute="JumpBackToPrevZoom_CanExecuted"
/>
<CommandBinding
Command="{StaticResource Commands.FitContent}"
Executed="FitContent_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.Fill}"
Executed="Fill_Executed"
/>
<CommandBinding
Command="{StaticResource Commands.OneHundredPercent}"
Executed="OneHundredPercent_Executed"
/>
</Window.CommandBindings>
<Window.ContextMenu>
<ContextMenu>
<!-- Clicking this menu item creates a new node. -->
<MenuItem
Header="Create Node"
Command="{StaticResource Commands.CreateNode}"
ToolTip="Creates a new node"
/>
<Separator />
<MenuItem
Header="Fit"
Command="{StaticResource Commands.FitContent}"
ToolTip="Fit selected nodes to the viewport, when nothing is selected fits all nodes to the viewport"
/>
<MenuItem
Header="Fill"
Command="{StaticResource Commands.Fill}"
ToolTip="Fit the entire content area to the viewport"
/>
<MenuItem
Header="100%"
Command="{StaticResource Commands.OneHundredPercent}"
ToolTip="Scale the content to 100%"
/>
<MenuItem
Header="Previous Zoom"
Command="{StaticResource Commands.JumpBackToPrevZoom}"
ToolTip="Return to the previous zoom level"
/>
<Separator />
<MenuItem
Header="Zoom Out"
Command="{StaticResource Commands.ZoomOut}"
ToolTip="Zooms out from the canvas"
/>
<MenuItem
Header="Zoom In"
Command="{StaticResource Commands.ZoomOut}"
ToolTip="Zooms in on the canvas"
/>
</ContextMenu>
</Window.ContextMenu>
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<DockPanel>
<!-- Grid placed below the zoom and pan control that contains the zoom slider, zoom label and some buttons. -->
<Grid
Margin="5,5,5,5"
DockPanel.Dock="Bottom"
>
<Grid.ColumnDefinitions>
<!-- empty space -->
<ColumnDefinition Width="*" />
<!-- the fit content button -->
<ColumnDefinition Width="40" />
<!-- spacer -->
<ColumnDefinition Width="5" />
<!-- the fill button -->
<ColumnDefinition Width="40" />
<!-- spacer -->
<ColumnDefinition Width="5" />
<!-- the 100% button -->
<ColumnDefinition Width="40" />
<!-- spacer -->
<ColumnDefinition Width="5" />
<!-- the zoom percentage label -->
<ColumnDefinition Width="25" />
<!-- the % symbol -->
<ColumnDefinition Width="15" />
<!-- spacer -->
<ColumnDefinition Width="5" />
<!-- the zoom out button -->
<ColumnDefinition Width="20" />
<!-- spacer -->
<ColumnDefinition Width="5" />
<!-- the zoom slider -->
<ColumnDefinition Width="120" />
<!-- spacer -->
<ColumnDefinition Width="5" />
<!-- the zoom in button -->
<ColumnDefinition Width="20" />
<!-- spacer -->
<ColumnDefinition Width="10" />
<!-- resize grip -->
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!--
The 'fit content' button. Causes the content to be scaled so that all the graph nodes fit in the viewport.
-->
<Button
Grid.Column="1"
Command="{StaticResource Commands.FitContent}"
ToolTip="Fit all nodes to the viewport"
>
Fit
</Button>
<!--
The fill button. Causes the content to be scaled so that it fits in the viewport.
-->
<Button
Grid.Column="3"
Command="{StaticResource Commands.Fill}"
ToolTip="Fit the entire content area to the viewport"
>
Fill
</Button>
<!--
The 100% button. Causes the content to be scaled to 100 percent.
-->
<Button
Grid.Column="5"
Command="{StaticResource Commands.OneHundredPercent}"
ToolTip="Scale the content to 100%"
>
100%
</Button>
<!--
This is the label that shows what the current zoom level
is while zooming in and out.
-->
<TextBlock
Grid.Column="7"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Text="{Binding ElementName=zoomAndPanControl, Path=ContentScale, Converter={StaticResource scaleToPercentConverter}}"
/>
<TextBlock
Grid.Column="8"
VerticalAlignment="Center"
Text="%"
/>
<!-- Button to zoom out. -->
<Button
Grid.Column="10"
Command="{StaticResource Commands.ZoomOut}"
ToolTip="Zoom out from the content"
>
-
</Button>
<!-- Slider to change the current zoom level. -->
<Slider
Grid.Column="12"
Minimum="10"
LargeChange="20"
TickFrequency="10"
Maximum="200"
SmallChange="10"
TickPlacement="TopLeft"
Value="{Binding ElementName=zoomAndPanControl, Path=ContentScale, Converter={StaticResource scaleToPercentConverter}}"
ToolTip="Change the zoom level of the content"
/>
<!--
Button to zoom in.
-->
<Button
Grid.Column="14"
Command="{StaticResource Commands.ZoomIn}"
ToolTip="Zoom in on the content"
>
+
</Button>
<!-- Stick a resize grip in the bottom right corner of the window. -->
<ResizeGrip
Grid.Column="16"
/>
</Grid>
<!--
The ScrollViewer displays scrollbars when the content is too big to fit in the viewport.
Focusable is set to False because it is only that NetworkView that I want to be focusable.
-->
<ScrollViewer
CanContentScroll="True"
VerticalScrollBarVisibility="Visible"
HorizontalScrollBarVisibility="Visible"
Focusable="False"
>
<!--
The ZoomAndPanControl displays the NetworkView as its content.
We can use the mouse to zoom and pan about the NetworkView.
-->
<ZoomAndPan:ZoomAndPanControl
x:Name="zoomAndPanControl"
ContentScale="{Binding ContentScale, Mode=TwoWay}"
ContentOffsetX="{Binding ContentOffsetX, Mode=TwoWay}"
ContentOffsetY="{Binding ContentOffsetY, Mode=TwoWay}"
ContentViewportWidth="{Binding ContentViewportWidth, Mode=OneWayToSource}"
ContentViewportHeight="{Binding ContentViewportHeight, Mode=OneWayToSource}"
Background="LightGray"
Focusable="False"
>
<!--
We need to nest our NetworkView in an AdornerDecorator so that adorners work correct when
we zoom in and out using the ZoomAndPanControl
-->
<AdornerDecorator>
<!--
This grid specifies the size of the ZoomAndPanControl's content.
It wraps up both the NetworkView and drag-zooming Canvas.
-->
<Grid
Width="{Binding ContentWidth}"
Height="{Binding ContentHeight}"
>
<!-- In this sample the NetworkView is the content displayed by the ZoomAndPanControl. -->
<NetworkUI:NetworkView
x:Name="networkControl"
NodesSource="{Binding Network.Nodes}"
ConnectionsSource="{Binding Path=Network.Connections}"
ConnectionDragStarted="networkControl_ConnectionDragStarted"
QueryConnectionFeedback="networkControl_QueryConnectionFeedback"
ConnectionDragging="networkControl_ConnectionDragging"
ConnectionDragCompleted="networkControl_ConnectionDragCompleted"
MouseDown="networkControl_MouseDown"
MouseUp="networkControl_MouseUp"
MouseMove="networkControl_MouseMove"
MouseWheel="networkControl_MouseWheel"
/>
<!--
This Canvas and Border are used as a very simple way to render a drag rectangle that the user
uses to specify an area to zoom in on.
-->
<Canvas
x:Name="dragZoomCanvas"
Visibility="Collapsed"
>
<Border
x:Name="dragZoomBorder"
BorderBrush="Black"
BorderThickness="1"
Background="Silver"
CornerRadius="1"
Opacity="0"
/>
</Canvas>
</Grid>
</AdornerDecorator>
</ZoomAndPan:ZoomAndPanControl>
</ScrollViewer>
</DockPanel>
</Window>
|
By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.
If a file you wish to view isn't highlighted, and is a text file (not binary), please
let us know and we'll add colourisation support for it.
Software craftsman | Author | Writing rapidfullstackdevelopment.com - Posting about how to survive and flourish as a software developer
Follow on Twitter for news and updates: https://twitter.com/codecapers
I'm writing a new book: Rapid Fullstack Development. Learn from my years of experience and become a better developer.
My second book, Bootstrapping Microservices, is a practical and project-based guide to building distributed applications with microservices.
My first book Data Wrangling with JavaScript is a comprehensive overview of working with data in JavaScript.
Data-Forge Notebook is my notebook-style application for data transformation, analysis and transformation in JavaScript.
I have a long history in software development with many years in apps, web apps, backends, serious games, simulations and VR. Making technology work for business is what I do: building bespoke software solutions that span multiple platforms.
I have years of experience managing development teams, preparing technical strategies and creation of software products. I can explain complicated technology to senior management. I have delivered cutting-edge products in fast-paced and high-pressure environments. I know how to focus and prioritize to get the important things done.
Author
- Rapid Fullstack Development
- Bootstrapping Microservices
- Data Wrangling with JavaScript
Creator of Market Wizard
- https://www.market-wizard.com.au/
Creator of Data-Forge and Data-Forge Notebook
- http://www.data-forge-js.com
- http://www.data-forge-notebook.com
Web
- www.codecapers.com.au
Open source
- https://github.com/ashleydavis
- https://github.com/data-forge
- https://github.com/data-forge-notebook
Skills
- Quickly building MVPs for startups
- Understanding how to get the most out of technology for business
- Developing technical strategies
- Management and coaching of teams & projects
- Microservices, devops, mobile and fullstack software development