65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF: A* 搜索

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (125投票s)

2009 年 11 月 8 日

CPOL

12分钟阅读

viewsIcon

317330

downloadIcon

5904

使用 A* 算法的人工智能搜索应用程序。

目录

引言

我不知道有多少人了解我的过去,但我的学位是人工智能和计算机科学(CSAI)。我们的一项作业是创建一个搜索,搜索伦敦地铁站的精简列表。当时,我觉得我的文本搜索很棒,但忍不住想用漂亮的 UI 代码重新尝试一下。然后,WPF 应运而生,我开始接触它,开始撰写相关文章,并忘记了我所有的 CSAI 日子。

时间流逝……呼呼

我去了度假,开心的日子,假期结束了。糟了,又要工作了。

所以我想我需要一些有趣的东西来让我回到工作状态,我想,我知道我会再次尝试伦敦地铁的那个代理,这次要使用 WPF 来实现高度图形化,当然了。

我知道,这是一篇非常小众的文章,对其他人来说重用性不高,但我可以向您保证,如果您想学习 WPF,这是一个很好的 WPF 功能及其实现方式的范例。所以它还是有价值的。

总之,这篇文章,正如您现在可能已经想象到的,是关于伦敦地铁搜索算法的。它实际上是一个类似 A* 的算法,其中搜索由已知成本和启发式引导,这是一种自定义的引导因子。

这篇文章中有一些我认为是 WPF 良好实践的技巧,还有一些您可能已经使用过或未曾使用过的 XLINQ/LINQ 片段。

所以,我希望您对此有足够的兴趣,继续阅读。我个人很喜欢它。

它看起来是什么样的?

啊,这才是漂亮的部分。我必须说,我本人对它的外观非常满意。这里有几张截图。以下是亮点列表

  • 平移
  • 缩放
  • 车站连接提示
  • 隐藏不需要的线条(以简化图表)
  • 信息气泡
  • 图解式解决方案路径可视化
  • 文本解决方案路径描述

首次启动时,线条绘制如下

找到解决方案路径后,它也会在图表上绘制,使用更宽(希望如此)更可见的笔刷,如下所示。另外值得注意的是,每个车站都有一个漂亮的工具提示,显示其自身的线路连接。

找到解决方案路径后,UI 右下角会显示一个新的信息按钮,用户可以使用。

此信息按钮允许用户查看解决方案路径的文本描述。

找到解决方案路径后,用户还可以隐藏不是解决方案路径一部分的线条,这是通过上下文菜单(右键单击)实现的,该菜单会显示线条弹出窗口(如下图所示),用户可以从中切换任何允许切换的线条的可见性(基本上不是解决方案路径的一部分)。解决方案路径中的线条将被禁用,因此用户无法切换它们。

它做什么以及如何做?

在接下来的部分中,我将介绍所有独立的部分如何协同工作形成一个完整的应用程序。

加载数据

所以,很明显,首先需要做的事情之一就是加载一些数据。数据实际上分为两部分

  • 车站地理坐标:存储在名为“StationLocations.csv”的 CSV 文件中。这些信息可在http://www.doogal.co.uk/london_stations.php找到。
  • 线路连接:我 painstakingly 手动创建了一个名为“StationConnections.xml”的 XML 文件。

为了加载这些数据,这两个文件被嵌入为资源,并使用不同的技术读出。初始数据的读取位于 `ThreadPool` 中的一个新工作项内,这允许 UI 在数据加载时保持响应,即使 UI 或用户在数据加载之前能做的事情不多。但这样做是很好的实践。

那么数据是如何加载的呢?好吧,我们像这样将我们的工作程序入队,这发生在名为 `TubeStationsCanvas` 的专用 `Canvas` 控件中,该控件位于另一个名为 `DiagramViewer` 的控件内,而 `DiagramViewer` 又位于 `MainWindow` 中,其 ViewModel 称为 `PlannerViewModel` 并设置为其 `DataContext`。 `PlannerViewModel` 是应用程序的中心骨干对象。`PlannerViewModel` 存储一个 `Dictionary`。因此,应用程序中的大多数其他控件都将在某个阶段需要与 `PlannerViewModel` 进行交互。这有时通过直接的 XAML 绑定来实现,或者在其他情况下,则如以下所示实现,我们获取当前 `Window` 并获取其 `DataContext`,然后将其强制转换为 `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);

传递给工作程序的 `state` 对象是 `MainWindow` `DataContext` 使用的主 `PlannerViewModel` 的实例。`PlannerViewModel` 是协调所有用户操作的顶层 ViewModel。

回到数据加载方式,车站本身是通过读取“StationLocations.csv”文件的嵌入资源流来加载的。您也可以从下面的代码中看到,读取的坐标会根据 `TubeStationsCanvas` 的宽度和高度进行缩放,以正确地定位车站。基本上,发生的情况是,对于从 CSV 文件读取的每个车站,一个新的 `StationViewModel` 会被添加到 `PlannerViewModel` 的 `Dictionary` 中。然后,创建一个新的 `StationControl`,并将其 `DataContext` 设置为添加到 `PlannerViewModel` 的 `Dictionary` 中的 `StationViewModel`。然后,使用一些 DP 在 `TubeStationsCanvas` 中定位 `StationControl`。

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);
    }
});

车站连接使用 XLINQ 读取,如下所示,这是对“StationConnections.xml”文件的嵌入资源流进行的。发生的情况是,从 XML 文件读取的车站会从 `PlannerViewModel` 的 `Dictionary` 中检索,然后检索到的 `StationViewModel` 会为其当前从 XML 文件读取的线路添加连接。对所有读取的线路和线路上的车站重复此操作。

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));
    }
}

绘制线条

正如我刚才提到的,有一个名为 `TubeStationsCanvas` 的专用 `Canvas` 控件,它的工作是渲染车站连接和搜索解决方案路径。我们刚刚介绍了车站最初是如何加载的,那么连接是如何绘制的呢?诀窍在于知道起始车站是什么。这也发生在 `TubeStationsCanvas` 中,在尝试渲染任何连接之前。我们以一条线路为例,比如 Jubilee 线,看看它是如何工作的。

首先,我们在 `TubeStationsCanvas` 中为线路设置一个起始车站,如下所示

List<StationViewModel> jublieeStarts = new List<StationViewModel>();
jublieeStarts.Add(vm.Stations["Stanmore"]);
startStationDict.Add(Line.Jubilee, jublieeStarts);

通过这样做,我们可以获取线路的起点,然后使用一些 LINQ 来获取所有连接。

以下是 Jubilee 线的一个例子;同样,这一切都在 `TubeStationsCanvas` 中完成。在这种情况下,它使用了 `void OnRender(DrawingContext dc)` 的重写,该方法公开了一个 `DrawingContext` 参数(想想 WinForms 中的 `OnPaint()`),它允许我们绘制东西。

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");
    }
}

可以看到,它使用了名为 `DrawLines()` 的方法,所以让我们继续我们的绘图之旅。

private void DrawLines(DrawingContext dc, Line theLine,
        SolidColorBrush brush, IEnumerable<StationViewModel> stations)
{
    foreach (var connectedStation in stations)
    {
        DrawConnectionsForLine(theLine, dc, brush, connectedStation);
    }
}

这反过来又调用了 `DrawConnectionsForLine()` 方法,如下所示

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);

}

可以看到 `DrawConnectionsForLine()` 方法是递归调用的,确保**所有**连接都被绘制。

这就是所有线路的连接绘制方式。

平移图表

正如我在文章开头提到的,图表组件支持平移。这比想象的要容易实现。它基本上都归结为一个名为 `PanningScrollViewer` 的专用 `ScrollViewer`,它包含 `TubeStationsCanvas`。 `PanningScrollViewer` 的所有代码如下所示

/// <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
}

此外,还有一个小的 XAML `Style` 是必需的,如下所示

<!-- scroll viewer -->
<Style x:Key="ScrollViewerStyle"
       TargetType="{x:Type ScrollViewer}">
    <Setter Property="HorizontalScrollBarVisibility"
            Value="Hidden" />
    <Setter Property="VerticalScrollBarVisibility"
            Value="Hidden" />
</Style>

这是托管 `TubeStationsCanvas` 的 `PanningScrollViewer`

<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>

缩放图表

缩放,虽然要归功于别人,但几乎所有 WPF 缩放应用程序的缩放功能都可能来源于 Vertigo 出色的 WPF 范例 The Family Show,它曾经也是,现在仍然是我见过的 WPF 的最佳用途之一。

总之,长话短说,缩放是通过标准的 WPF `Slider` 控件实现的(尽管我用 XAML `Style` 对它进行了一些修饰)以及 `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();
        }
    }
}

用户可以使用应用程序中显示的 `Slider` 进行缩放

车站线路提示

我认为让每个 `StationControl` 渲染一个 **仅** 显示该 `StationControl` 所属线路的工具提示会很好。这是使用标准绑定完成的,其中 `StationControl` 的 `ToolTip` 绑定到 `StationViewModel`,后者是 `StationControl` 的 DataContext。`StationViewModel` 包含一个 `IList`,代表车站所在的线路。所以从那里开始,只是让工具提示看起来很酷的问题。幸运的是,这是 WPF 的强项。这是如何实现的

首先,`StationControl.ToolTip`,定义如下

<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>

并且在 `* /Resources/AppStyles.xaml` `ResourceDictionary` 中有一个 `Style`,它决定了 `ToolTip` 的外观。这是 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>

当您将鼠标悬停在 `StationControl` 上时,它看起来很酷;它会像这样,当您将鼠标悬停在图表上的不同车站时,这非常有用。哦,另外,当您将鼠标悬停在 `StationControl` 上时,实际的 `StationControl` 会变大变小;这也有助于向用户显示 `ToolTip` 对应的车站。

引导式搜索

正如我之前提到的,搜索是 A* 类型搜索,我们有一个已知成本和一个未知成本。维基百科对 A* 算法的说法如下

在计算机科学中:**A***(发音为“A star”)是一种最佳优先图搜索算法,它找到从给定的初始节点到其中一个目标节点(在可能的一个或多个目标中)的最短路径。

它使用一个距离加成本启发式函数(通常表示为 f(x))来确定搜索访问树中节点的顺序。距离加成本启发式是两个函数的总和

  • 路径成本函数,即从起始节点到当前节点的成本(通常表示为 g(x))。
  • 以及到目标的距离的容许“启发式估计”(通常表示为 h(x))。

f(x) 函数的 h(x) 部分必须是容许启发式;也就是说,它不能高估到目标的距离。此外,h(x) 必须使得在采取新步骤时 f(x) 不会减小。因此,对于像路由这样的应用程序,h(x) 可能表示到目标的直线距离,因为这是任意两个点(或节点)之间的物理上最小的可能距离。

--http://en.wikipedia.org/wiki/A*_search_algorithm

搜索算法,如在应用程序中使用,由下图描述。搜索是从 `PlannerViewModel` 中的一个 `ICommand` 启动的,该命令由用户单击 UI 上的按钮触发。

实际代码如下所示

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;

}

可以看到搜索返回一个 `SearchPath` 对象。成本存储在 `SearchPath` 对象中。搜索算法有几个相关的成本。

HCost

到目标的距离的容许“启发式估计”(通常表示为 h(x))。

在附加的应用程序中,这只是两个对象之间的差值,其代码如下

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

路径成本函数,即从起始节点到当前节点的成本(通常表示为 *g*(x))。

在附加的应用程序中,这是通过到达当前车站的线路更改次数 * SOME_CONSTANT 来计算的。

public Double GCost
{
    get { return gCost + (changesSoFar * LINE_CHANGE_PENALTY); }
}

FCost

这是指导搜索在 Agenda 中的 `SearchPath` 被排序时的魔术数字。FCost 不过是 GCost + FCost。因此,通过根据此值对 Agenda `SearchPath` 进行排序,可以选择所谓的最佳路线。

绘制解决方案路径

绘制解决方案路径类似于我们之前看到的绘制实际线路的方式,但这次我们只遍历 SolutionFound `SearchPath` 对象中的 `StationViewModel`,并在解决方案路径中的 `StationViewModel` 中绘制连接。

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);
    }
}

这样在图表上看起来是这样的;请注意绘制的橙色路径,那是解决方案路径

隐藏不需要的线条

虽然在图表中看到所有线路都很好,但有很多东西需要显示,如果我们能隐藏那些不是 SolutionFound `SearchPath` 一部分的不需要的线路,那就更好了。为此,附加的演示代码包含一个带有复选框的弹出窗口,每个线路一个复选框,如果 SolutionFound `SearchPath` 不包含复选框所指的线路,则该复选框才会被启用。

假设 SolutionFound `SearchPath` 不包含某条线路,用户可以通过复选框切换该线路的 `Visibility`。

以下是线路可见性弹出窗口的屏幕截图以及取消选中其中一个复选框的效果

作为附加奖励,我还包含了一个附加行为,它允许用户通过嵌入的 `Thumb` 控件拖动弹出窗口。该附加行为如下所示

/// <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

}

要使用此附加行为,弹出窗口本身必须使用特定的 `Style`;如下所示

<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>

文本结果

一直以来,我都想创建一个华丽的弹出窗口来保存找到的解决方案路径的文本描述,让用户能够阅读英文描述。这个想法很简单,对于 SolutionFound `SearchPath` 上的每个 `StationViewModel`:迭代并构建路线的文本描述,然后显示给用户。这在实际的 `SearchPath` 对象中如下所示

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();
}

在那之后,剩下要做的就是显示它。我前几天在我的博客上谈到了我是如何做到的:http://sachabarber.net/?p=580。基本思想是使用一个弹出窗口并使其支持透明度,然后绘制一个呼叫图形 `Path`,并显示 `SearchPath` 文本。

这是它的实际运行效果。我觉得它看起来很酷

所有这些都是使用以下 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>

就是这样。希望您喜欢。

总之,就这些了。希望您喜欢。即使您没有伦敦地铁规划器的用途,我认为这仍然有很多 WPF 学习的好材料。

谢谢

一如既往,欢迎投票/评论。

© . All rights reserved.