I don't know how many of you know about my past, but I studied Artificial Intelligence and Computer Science (CSAI) for my degree. One of the assigments we had was to create a search over a cut down list of the London undergound stations. At the time I thought my textual search was ace, but I couldn't help but think it would be nice to have a go at that again using some nice UI code. Then low and behold WPF came along, and I got into it and started writing articles in it, and forgot all about my CSAI days.
So time passes....zzzzzz
I went on holiday, happy days, holiday end. bummer, back to work.
So I thought I need something fun to get myself back into work mode, so I thought, I know I'll have a go at the London underground tube agent again, this time making it highly graphical using WPF of course.
Now I know this is quite a niche article and it is not that re-usable to anyone out there, but I can assure you if you want to learn some WPF this is a good exemplar of what WPF can do, and how. So there is some merit in it.
Anyways, this article as you can now probably imagine is all about a London underground search algorithm. It is actually a A* like algorithm, where the search is guided by a known cost and a heuristic, which is some custom guidance factor.
There are some neat tricks in this article that I feel are good WPF practice and also some XLINQ/LINQ snippets which you may or may not have used before.
So I hope there is enough interest in there for you, to carry on reading. I like it anyway.
Ah now that is the nice part, I have to say I for one am most pleased with how it looks. Here are a couple of screen shots. And he is the list of the highlights:
When it first starts the lines are drawn as shown here
When a solution path is found, it is also drawn on the diagram using a wider (hopefully) more visible brush, which you can see below. Also of note is that each station also has a nice tooltip which shows its own line connections
When a solution path is found, a new information button will be shown in the bottom right of the UI which the user can use.
This information button allows the user to see a textual description of the solution path
When a solution path is found, it is also possible for the user to hide lines that are not part of the solution path which is achieved by using a context menu (right click) which will show the lines popup (as seen below), from where the user can toggle the visibility of any line that is allowed to be toggled (basically not part of the solution path). The lines that are part of the solution path will be disabled, so the user will not be able to toggle them.
In the next sections I will cover how all the individual parts work together to form a whole app.
So obviously one of the 1st things that needs to happen is that we need to load some data. The data is actually split into 2 parts
In order to load this data these 2 files are embedded as resources and read
out using different techniques. The reading of the initial data is inside a new
worker item in the ThreadPool. Which allows the UI to remain
responsive while the data is loading, even though there is not much the UI or
user can do until the data is loaded. Still it's good practice.
So how is the data loaded. Well we enqueue our worker like this, where this
is happening in a specialized Canvas control called TubeStationsCanvas which is
located inside another called DiagramViewer which in turn is inside the
MainWindow who has a ViewModel called PlannerViewModel set as it's DataContext.
The PlannerViewModel is the central backbone object of the application.
The PlannerViewModel stores a
Dictionary<String,StationViewModel>. As such most other controls within the app, will need to
interact with the PlannerViewModel at some stage, this is sometimes achieved
using direct XAML Binding, or in some other cases is realized as shown below, where
we get the current Window and get its DataContext which is cast to a PlannerViewModel.
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
return;
vm = (PlannerViewModel)Window.GetWindow(this).DataContext;
ThreadPool.QueueUserWorkItem((stateObject) =>
{
#region Read in stations
......
......
#endregion
#region Setup start stations
......
......
#endregion
#region Setup connections
......
......
#endregion
#region Update UI
......
......
#endregion
}, vm);
Where the state object being passed to the worker is an instance of the main
PlannerViewModel that is being used by the
MainWindow DataContext. The
PlannerViewModelis the top level ViewModel that coordinates all user actions.
Anyway getting back to how the data is loaded, the stations themselves are
loaded by reading the embedded resource stream for the "StationLocations.csv"
file. It can also be seen below that the read in coordinates are scaled against
the TubeStationsCanvas Width and Height to allow the stations to be
positioned correctly. Basically what happens is that for each station read in
from the CSV file a new StationViewModel is added to the Dictionary<String,StationViewModel> in the PlannerViewModel.
Then a new StationControl is created and has its DataContext set to the
StationViewModel which was added to the Dictionary<String,StationViewModel> in the PlannerViewModel.
And then the StationControlis positioned within the TubeStationsCanvas using some DPs.
vm.IsBusy = true;
vm.IsBusyText = "Loading Stations";
Assembly assembly = Assembly.GetExecutingAssembly();
using (
TextReader textReader =
new StreamReader(assembly.GetManifestResourceStream
("TubePlanner.Data.StationLocations.csv")))
{
String line = textReader.ReadLine();
while (line != null)
{
String[] parts = line.Split(',');
vm.Stations.Add(parts[0].Trim(),
new StationViewModel(parts[0].Trim(),
parts[1].Trim(),
Int32.Parse(parts[2]),
Int32.Parse(parts[3]),
parts[4].Trim()));
line = textReader.ReadLine();
}
}
Decimal factorX = maxX - minX;
Decimal factorY = maxY - minY;
this.Dispatcher.InvokeIfRequired(() =>
{
Double left;
Double bottom;
foreach (var station in vm.Stations.Values)
{
StationControl sc = new StationControl();
left = this.Width *
(station.XPos - minX) / (maxX - minX);
bottom = this.Height *
(station.YPos - minY) / (maxY - minY);
left = left > (this.Width - sc.Width)
? left - (sc.Width)
: left;
sc.SetValue(Canvas.LeftProperty, left);
bottom = bottom > (this.Height - sc.Height)
? bottom - (sc.Height)
: bottom;
sc.SetValue(Canvas.BottomProperty, bottom);
station.CentrePointOnDiagramPosition =
new Point(left + sc.Width / 2,
(this.Height - bottom) - (sc.Height / 2));
//set DataContext
sc.DataContext = station;
//add it to Canvas
this.Children.Add(sc);
}
});
And the station connections are read in as follows using some XLINQ over the embedded resource stream for the "StationConnections.xml"
file. What happens is that the station read in from the XML file is retrieved
from the Dictionary<String,StationViewModel> in the PlannerViewModel,
and then the retreived StationViewModel has its connections added
to for the currently read line from the XML file. This is repeated for all read
in lines and the stations on the read in lines.
XElement xmlRoot = XElement.Load("Data/StationConnections.xml");
IEnumerable<XElement> lines = xmlRoot.Descendants("Line");
//For each Line get the connections
foreach (var line in lines)
{
Line isOnLine = (Line)Enum.Parse(typeof(Line),
line.Attribute("Name").Value);
//now fetch the Connections for all the Stations on the Line
foreach (var lineStation in line.Elements())
{
//Fetch station based on it's name
StationViewModel currentStation =
vm.Stations[lineStation.Attribute("Name").Value];
//Setup Line for station
if (!currentStation.Lines.Contains(isOnLine))
currentStation.Lines.Add(isOnLine);
//Setup Connects to, by fetching the Connecting Station
//and adding it to the currentStations connections
StationViewModel connectingStation =
vm.Stations[lineStation.Attribute("ConnectsTo").Value];
if (!connectingStation.Lines.Contains(isOnLine))
connectingStation.Lines.Add(isOnLine);
currentStation.Connections.Add(
new Connection(connectingStation, isOnLine));
}
}
As I just mentioned there is a specialized Canvas control called TubeStationsCanvas
whos job it is to render the station connections and the searches solution path.
We just covered how the stations are actually loaded in the first place, so how
do the connection get drawn. Well the trick is to know about what the start
Stations are. This is also done in the TubeStationsCanvas prior to
attempting to render any connections. If we consider one line as an example, say
Jubilee, and look at how that works.
First we set up a start Station for the line in the TubeStationsCanvas
as follows:
List<StationViewModel> jublieeStarts = new List<StationViewModel>();
jublieeStarts.Add(vm.Stations["Stanmore"]);
startStationDict.Add(Line.Jubilee, jublieeStarts);
By doing this it allows us to grab the start of a line, and from there grab all the connections using a bit of LINQ.
Here is an example for the Jubilee line, again this is all done in the TubeStationsCanvas. In this case it is using an override of the
void OnRender(DrawingContext dc), which exposes a DrawingContext parameter (think OnPaint() in winforms) which allows us to well er, Draw stuff.
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
try
{
if (isDataReadyToBeRendered && vm != null)
{
.....
.....
.....
//Jubilee line
if (vm.JubileeRequestedVisibility)
{
var jublieeStartStations = startStationDict[Line.Jubilee];
DrawLines(dc, Line.Jubilee, Brushes.Silver, jublieeStartStations);
}
.....
.....
.....
}
}
catch (Exception ex)
{
messager.ShowError("There was a problem setting up the Stations / Lines");
}
}
It can be seen that this makes use of a method called DrawLines(), so let's
continue our Drawing journey.
private void DrawLines(DrawingContext dc, Line theLine,
SolidColorBrush brush, IEnumerable<StationViewModel> stations)
{
foreach (var connectedStation in stations)
{
DrawConnectionsForLine(theLine, dc, brush, connectedStation);
}
}
And this in turn calls the DrawConnectionsForLine() method, which is shown below
private void DrawConnectionsForLine(Line theLine, DrawingContext dc,
SolidColorBrush brush, StationViewModel startStation)
{
Pen pen = new Pen(brush, 25);
pen.EndLineCap = PenLineCap.Round;
pen.DashCap = PenLineCap.Round;
pen.LineJoin = PenLineJoin.Round;
pen.StartLineCap = PenLineCap.Round;
pen.MiterLimit = 8;
var connections = (from x in startStation.Connections
where x.Line == theLine
select x);
foreach (var connection in connections)
{
dc.DrawLine(pen,
startStation.CentrePointOnDiagramPosition,
connection.Station.CentrePointOnDiagramPosition);
}
foreach (var connection in connections)
DrawConnectionsForLine(theLine, dc, brush, connection.Station);
}
It can be seen that the DrawConnectionsForLine() method, is called recursively, ensuring ALL connections are
drawn.
That is how the connection drawing is done for all lines.
As I stated at the start of the article the diagramming component supports
panning. This is realized much easier than one would think. Its bascally all
down to a specialized ScrollViewer called PanningScrollViewer, which contains
the TubeStationsCanvas. The entire code for the PanningScrollViewer
is as shown below.
/// <summary>
/// A panning scrollviewer
/// </summary>
public class PanningScrollViewer : ScrollViewer
{
#region Data
// Used when manually scrolling
private Point scrollStartPoint;
private Point scrollStartOffset;
#endregion
#region Mouse Events
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
if (IsMouseOver)
{
// Save starting point, used later when determining how much to scroll.
scrollStartPoint = e.GetPosition(this);
scrollStartOffset.X = HorizontalOffset;
scrollStartOffset.Y = VerticalOffset;
// Update the cursor if can scroll or not.
this.Cursor = (ExtentWidth > ViewportWidth) ||
(ExtentHeight > ViewportHeight) ?
Cursors.ScrollAll : Cursors.Arrow;
this.CaptureMouse();
}
base.OnPreviewMouseDown(e);
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (this.IsMouseCaptured)
{
// Get the new scroll position.
Point point = e.GetPosition(this);
// Determine the new amount to scroll.
Point delta = new Point(
(point.X > this.scrollStartPoint.X) ?
-(point.X - this.scrollStartPoint.X) :
(this.scrollStartPoint.X - point.X),
(point.Y > this.scrollStartPoint.Y) ?
-(point.Y - this.scrollStartPoint.Y) :
(this.scrollStartPoint.Y - point.Y));
// Scroll to the new position.
ScrollToHorizontalOffset(this.scrollStartOffset.X + delta.X);
ScrollToVerticalOffset(this.scrollStartOffset.Y + delta.Y);
}
base.OnPreviewMouseMove(e);
}
protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
{
if (this.IsMouseCaptured)
{
this.Cursor = Cursors.Arrow;
this.ReleaseMouseCapture();
}
base.OnPreviewMouseUp(e);
}
#endregion
}
Where there is also a small XAML Style required which is as follows:
<!-- scroll viewer -->
<Style x:Key="ScrollViewerStyle"
TargetType="{x:Type ScrollViewer}">
<Setter Property="HorizontalScrollBarVisibility"
Value="Hidden" />
<Setter Property="VerticalScrollBarVisibility"
Value="Hidden" />
</Style>
And here is the PanningScrollViewer hosting the TubeStationsCanvas
<local:PanningScrollViewer x:Name="svDiagram" Visibility="{Binding Path=IsBusy,
Converter={StaticResource BoolToVisibilityConv}, ConverterParameter=False}"
Style="{StaticResource ScrollViewerStyle}">
<local:TubeStationsCanvas x:Name="canv" Margin="50"
Background="Transparent"
Width="7680"
Height="6144">
</local:TubeStationsCanvas>
</local:PanningScrollViewer>
Zooming, well credit where credit is due, is more than likely sourced for all zooming WPF apps from Vertigos sublime initial WPF exemplar, the The Family Show, which was and still is one of the best uses of WPF I have seen.
Anyway to cut a long story short the zooming is realized using a standard WPF
Slidercontrol (albeit I have jazzed it up a bit with a XAML Style)
and a ScaleTransform.
public Double Zoom
{
get { return ZoomSlider.Value; }
set
{
if (value >= ZoomSlider.Minimum && value <= ZoomSlider.Maximum)
{
canv.LayoutTransform = new ScaleTransform(value, value);
canv.InvalidateVisual();
ZoomSlider.Value = value;
UpdateScrollSize();
this.InvalidateVisual();
}
}
}
The user can zoom using the Slider as shown in the app :

I thought it may be nice to have each StationControl render a tooltip which
ONLY shows the lines that the StationControl is on. This is done using standard
Binding, where the StationControls ToolTip binds to the StationViewModel which
is the DataContext for the StationControl. The StationViewModel contains a
IList<Line> which represents the lines the station is on. So from there it is
just a question of making the ToolTip look cool. Luckily this is what WPF is
good at. Here is how.
First the StationControl.ToolTip which is defined as shown below
<UserControl.ToolTip>
<Border HorizontalAlignment="Stretch"
SnapsToDevicePixels="True"
BorderBrush="Black"
Background="White"
VerticalAlignment="Stretch"
Height="Auto"
BorderThickness="3"
CornerRadius="5">
<Grid Margin="0" Width="300">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border CornerRadius="2,2,0,0" Grid.Row="0"
BorderBrush="Black" BorderThickness="2"
Background="Black">
<StackPanel Orientation="Horizontal" >
<Image Source="../Images/tubeIconNormal.png"
Width="30"
Height="30"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Margin="2"/>
<Label Content="{Binding DisplayName}"
FontSize="14"
FontWeight="Bold"
Foreground="White"
VerticalContentAlignment="Center"
Margin="5,0,0,0" />
</StackPanel>
</Border>
<StackPanel Orientation="Vertical" Grid.Row="1">
<!-- Bakerloo -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch"
Visibility="{Binding RelativeSource={RelativeSource Self},
Path=DataContext, Converter={StaticResource OnLineToVisibilityConv},
ConverterParameter='Bakerloo'}">
<Border BorderBrush="Black" BorderThickness="2"
CornerRadius="5"
Background="Brown" Height="30" Width="30"
Margin="10,2,10,2">
<Image Source="../Images/tubeIcon.png" Width="20"
Height="20" HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<Label Content="Bakerloo"
Padding="2"
Foreground="Black"
FontSize="10"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Center"/>
</StackPanel>
<!-- Other lines done the same as Bakerloo -->
<!-- Other lines done the same as Bakerloo -->
<!-- Other lines done the same as Bakerloo -->
<!-- Other lines done the same as Bakerloo -->
<!-- Other lines done the same as Bakerloo -->
<!-- Other lines done the same as Bakerloo -->
<Label Content="{Binding ZoneInfo}"
FontSize="10"
Foreground="Black"
FontWeight="Bold"
VerticalContentAlignment="Center"
Margin="5" />
</StackPanel>
</Grid>
</Border>
</UserControl.ToolTip>
And there is a Style in the /Resources/AppStyles.xaml ResourceDictionary which dictates what
ToolTips should look like. Here is that XAML
<!-- Tooltip-->
<Style TargetType="{x:Type ToolTip}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToolTip}">
<Border BorderBrush="Black" CornerRadius="3"
Margin="10,2,10,2"
Background="White" BorderThickness="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Label Content="{TemplateBinding Content}"
Padding="2"
Foreground="Black"
FontSize="10"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This looks quite cool when you hover over a StationControl, it will look like
the following, which is quite useful when you are mousing over various stations
on the diagram. Oh also the actual StationControl grows and shrinks
when you mouse over it, this also aids in showing the user which station the
ToolTip is for.

As I previously mentioned the search is a A* type of search where we have a known cost and a unknown cost, Wikipedia has this to say about A* algorithm.
In computer science, A* (pronounced "A star") is a best-first graph search algorithm that finds the least-cost path from a given initial node to one goal node(out of one or more possible goals).
It uses a distance-plus-cost heuristic function (usually denoted f(x)) to determine the order in which the search visits nodes in the tree. The distance-plus-cost heuristic is a sum of two functions:
The h(x) part of the f(x) function must be an admissible heuristic; that is, it must not overestimate the distance to the goal. Besides, h(x) must be such that f(x) cannot decrease when a new step is taken. Thus for an application like routing, h(x) might represent the straight-line distance to the goal, since that is physically the smallest possible distance between any two points (or nodes for that matter).
http://en.wikipedia.org/wiki/A*_search_algorithm
The search algorithm as used in the app, is described by the figure shown below.
The search is initiated from a ICommand in the PlannerViewModel which is
triggered from the user clicking the button on the UI.

Where the actual code looks like this:
public SearchPath DoSearch()
{
pathsSolutionsFound = new List<SearchPath>();
pathsAgenda = new List<SearchPath>();
SearchPath pathStart = new SearchPath();
pathStart.AddStation(vm.CurrentStartStation);
pathsAgenda.Add(pathStart);
while (pathsAgenda.Count() > 0)
{
SearchPath currPath = pathsAgenda[0];
pathsAgenda.RemoveAt(0);
if (currPath.StationsOnPath.Count(
x => x.Name.Equals(vm.CurrentEndStation.Name)) > 0)
{
pathsSolutionsFound.Add(currPath);
break;
}
else
{
StationViewModel currStation = currPath.StationsOnPath.Last();
List<StationViewModel> successorStations =
GetSuccessorsForStation(currStation);
foreach (StationViewModel successorStation in successorStations)
{
if (!currPath.StationsOnPath.Contains(successorStation) &&
!(ExistsInSearchPath(pathsSolutionsFound, successorStation)))
{
SearchPath newPath = new SearchPath();
foreach (StationViewModel station in currPath.StationsOnPath)
newPath.StationsOnPath.Add(station);
newPath.AddStation(successorStation);
pathsAgenda.Add(newPath);
pathsAgenda.Sort();
}
}
}
}
//Finally, get the best Path, this should be the 1st one found due
//to the heuristic evaluation performed by the search
if (pathsSolutionsFound.Count() > 0)
{
return pathsSolutionsFound[0];
}
return null;
}
It can be seen that the Search returns a SearchPath object. It
is in the SearchPath objects that the costs are stored. There are
several costs associated with the search algorithm.
HCost
An admissible "heuristic estimate" of the distance to the goal (usually denoted h(x)).
In terms of the attached application, this is simply the difference between two objects, which is coded as follows:
public static Double GetHCost(StationViewModel currentStation, StationViewModel endStation)
{
return Math.Abs(
Math.Abs(currentStation.CentrePointOnDiagramPosition.X -
endStation.CentrePointOnDiagramPosition.X) +
Math.Abs(currentStation.CentrePointOnDiagramPosition.Y -
endStation.CentrePointOnDiagramPosition.Y));
}
GCost
The path-cost function, which is the cost from the starting node to the current node (usually denoted g(x))
In terms of the attached application, this is calculated using the number of line changes to get to the current station * SOME_CONSTANT
public Double GCost
{
get { return gCost + (changesSoFar * LINE_CHANGE_PENALTY); }
}
FCost
This is the magic figure that guides the search, when the SearchPaths
in the Agenda are sorted. The FCost is nothing more than GCost + FCost. So by
sorting the Agenda SearchPaths on this value, the supposed best
route is picked.
Drawing the solution path is simliar to what we saw to draw the actual lines,
except this time, we are only looping through the StationViewModels of the
SolutionFound SearchPath object and drawing the connections in the
StationViewModels in the solution path.
private void DrawSolutionPath(DrawingContext dc, SearchPath solutionPath)
{
for (int i = 0; i < solutionPath.StationsOnPath.Count() - 1; i++)
{
StationViewModel station1 = solutionPath.StationsOnPath[i];
StationViewModel station2 = solutionPath.StationsOnPath[i + 1];
SolidColorBrush brush = new SolidColorBrush(Colors.Orange);
brush.Opacity = 0.6;
Pen pen = new Pen(brush, 70);
pen.EndLineCap = PenLineCap.Round;
pen.DashCap = PenLineCap.Round;
pen.LineJoin = PenLineJoin.Round;
pen.StartLineCap = PenLineCap.Round;
pen.MiterLimit = 10.0;
dc.DrawLine(pen,
station1.CentrePointOnDiagramPosition,
station2.CentrePointOnDiagramPosition);
}
}
This then looks like this on the diagram, note the orange path drawn, that is the Solution path.
Whilst it is nice to see all the lines in the diagram, there is a lot to
display and it would be nice if we could hide unwanted lines that are not part
of the SolutionFound SearchPath. To this end, the attached demo
code hosts a Popup window with a CheckBox per line, where the CheckBox for a
given line is only Enabled if the SolutionFound SearchPath does not
contain the line the checkbox is for.
Assuming that the SolutionFound SearchPath does not contain a
given line the user is able to toggle the lines Visibility using the CheckBox .
Here is a screen shot of the line Visibility Popup and the affect of
unchecking one of the CheckBoxes.

As an added bonus I have also included an attached behaviour that allows the
user to drag the Popup around using an embedded Thumb control.
That attached behaviour is shown below.
/// <summary>
/// Allows moving of Popup using a Thumb
/// </summary>
public class PopupBehaviours
{
#region IsMoveEnabled DP
public static Boolean GetIsMoveEnabledProperty(DependencyObject obj)
{
return (Boolean)obj.GetValue(IsMoveEnabledPropertyProperty);
}
public static void SetIsMoveEnabledProperty(DependencyObject obj, Boolean value)
{
obj.SetValue(IsMoveEnabledPropertyProperty, value);
}
// Using a DependencyProperty as the backing store for
//IsMoveEnabledProperty.
public static readonly DependencyProperty IsMoveEnabledPropertyProperty =
DependencyProperty.RegisterAttached("IsMoveEnabledProperty",
typeof(Boolean), typeof(PopupBehaviours),
new UIPropertyMetadata(false,OnIsMoveStatedChanged));
private static void OnIsMoveStatedChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
Thumb thumb = (Thumb)sender;
if (thumb == null) return;
thumb.DragStarted -= Thumb_DragStarted;
thumb.DragDelta -= Thumb_DragDelta;
thumb.DragCompleted -= Thumb_DragCompleted;
if (e.NewValue != null && e.NewValue.GetType() == typeof(Boolean))
{
thumb.DragStarted += Thumb_DragStarted;
thumb.DragDelta += Thumb_DragDelta;
thumb.DragCompleted += Thumb_DragCompleted;
}
}
#endregion
#region Private Methods
private static void Thumb_DragCompleted(object sender,
DragCompletedEventArgs e)
{
Thumb thumb = (Thumb)sender;
thumb.Cursor = null;
}
private static void Thumb_DragDelta(object sender,
DragDeltaEventArgs e)
{
Thumb thumb = (Thumb)sender;
Popup popup = thumb.Tag as Popup;
if (popup != null)
{
popup.HorizontalOffset += e.HorizontalChange;
popup.VerticalOffset += e.VerticalChange;
}
}
private static void Thumb_DragStarted(object sender,
DragStartedEventArgs e)
{
Thumb thumb = (Thumb)sender;
thumb.Cursor = Cursors.Hand;
}
#endregion
}
To use this attached behaviour the, Popup itself must use a particular Style this is shown below.
<Popup x:Name="popLines"
PlacementTarget="{Binding ElementName=mainGrid}"
Placement="Relative"
IsOpen="False"
Width="400" Height="225"
AllowsTransparency="True"
StaysOpen="False"
PopupAnimation="Scroll"
HorizontalAlignment="Right"
HorizontalOffset="30" VerticalOffset="30" >
<Border Background="Transparent" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="#FF000000"
BorderThickness="3"
CornerRadius="5,5,5,5">
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Thumb Grid.Row="0" Width="Auto" Height="40"
Tag="{Binding ElementName=popLines}"
local:PopupBehaviours.IsMoveEnabledProperty="true">
<Thumb.Template>
<ControlTemplate>
<Border Width="Auto" Height="40"
BorderBrush="#FF000000"
Background="Black" VerticalAlignment="Top"
CornerRadius="5,5,0,0" Margin="-2,-2,-2,0">
<Label Content="Lines"
FontSize="18"
FontWeight="Bold"
Foreground="White"
VerticalContentAlignment="Center"
Margin="5,0,0,0" />
</Border>
</ControlTemplate>
</Thumb.Template>
</Thumb>
<!-- Actual Content Grid-->
<Grid Grid.Row="1" Background="White"
Width="Auto" Height="Auto" Margin="0,0,0,10">
<ScrollViewer HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Visible">
</ScrollViewer>
</Grid>
</Grid>
</Border>
</Popup>
All along I knew I wanted to create a jazzy popup window, that would hold the
textual description of the solution path found, that would allow the user to
read the description in english. The idea is simple for each StationViewModel on
the SolutionFound SearchPath, iterate through and build up a text description of
the route and display it to the user. This is done in the actual SearchPath
object as follows:
private String GetPathDescription()
{
StringBuilder thePath = new StringBuilder(1000);
StationViewModel currentStation;
for (int i = 0; i < this.StationsOnPath.Count - 1; i++)
{
IList<Line> otherLines = this.StationsOnPath[i + 1].Lines;
List<Line> linesInCommon =
this.StationsOnPath[i].Lines.Intersect(otherLines).ToList();
if (i == 0)
{
currentStation = this.StationsOnPath[0];
thePath.Append("Start at " + currentStation.Name +
" station, which is on the " +
NormaliseLineName(linesInCommon[0]) + " line, ");
}
else
{
currentStation = this.StationsOnPath[i];
thePath.Append("\r\nThen from " + currentStation.Name +
" station, which is on the " +
NormaliseLineName(linesInCommon[0]) + " line, ");
}
thePath.Append("take the " + NormaliseLineName(linesInCommon[0]) +
" line to " + this.StationsOnPath[i + 1] + " station\r\n");
}
//return the path description
return thePath.ToString();
}
So after that all that is left to do is display it. I talked about how I do
that on my blog the other day,
http://sachabarber.net/?p=580. The basic idea is that you use a Popup and
get it to support Transparency and then draw a callout graphic Path, and show
the SearchPath text.
Here is in action, I think it looks pretty cool.

This is all done using the following XAML
<Popup x:Name="popInformation"
PlacementTarget="{Binding ElementName=imgInformation}"
Placement="Top"
IsOpen="False"
Width="400" Height="250"
AllowsTransparency="True"
StaysOpen="True"
PopupAnimation="Scroll"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalOffset="-190" VerticalOffset="-10" >
<Grid Margin="10">
<Path Fill="LightYellow" Stretch="Fill"
Stroke="LightGoldenrodYellow"
StrokeThickness="3" StrokeLineJoin="Round"
Margin="0" Data="M130,154 L427.5,154 427.5,
240.5 299.5,240.5 287.5,245.5 275.5,240.5 130,240.5 z">
<Path.Effect>
<DropShadowEffect BlurRadius="12" Color="Black"
Direction="315" Opacity="0.8"/>
</Path.Effect>
</Path>
<Grid Height="225" Margin="10,5,10,5"
HorizontalAlignment="Stretch" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" FontSize="16"
FontWeight="Bold" Content="Your Route"
HorizontalAlignment="Left" Margin="0,5,5,5"
VerticalAlignment="Center"/>
<Button Grid.Row="0" HorizontalAlignment="Right"
Width="25" Click="Button_Click"
Height="25" VerticalAlignment="Top" Margin="2">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="bord" CornerRadius="3"
BorderBrush="Transparent"
BorderThickness="2"
Background="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0" Width="25" Height="25">
<Label x:Name="lbl" Foreground="Black"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="Wingdings 2" Content="O"
FontSize="14"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="bord"
Property="BorderBrush"
Value="Black"/>
<Setter TargetName="bord"
Property="Background"
Value="Black"/>
<Setter TargetName="lbl"
Property="Foreground"
Value="LightGoldenrodYellow"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
<ScrollViewer Grid.Row="1" Margin="0,5,0,30"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" FontSize="10" Margin="5"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding Path=SearchPathDescription}"/>
</ScrollViewer>
</Grid>
</Grid>
</Popup>
Anyway there you go, hope you liked it. Even if you dont have a use for a London tube planner I think there is still lots of good WPF learning material in this one
As always votes / comments are welcome.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||