使用 Visual Basic 9.0 和 WPF 创建动态地图

来源:百度文库 编辑:神马文学网 时间:2024/04/26 12:12:51
本文讨论: WPF 依赖属性 使用 LINQ 查询 XML 绘制地图 实现地图数据可视化
本文使用了以下技术:
WPF, Visual Basic 9.0, LINQ
目录
WPF 数据绑定
应用程序对象模型
Map 类
导入地图数据
地图数据格式
导入数据
绘制地图
实现人口数据可视化
结论

一直对绘制地图有些着迷。同时我认为 Visual Studio® 2008 和 Visual Basic® 9.0 实在令人惊奇。因此,当我有机会体验 Visual Studio 并撰写相关文章时,我认为绝佳的主题是关于使用 Visual Basic 来绘制地图的教程。这不仅能够让我有机会得以展示一些非常酷的 Visual Basic 功能,而且还能为您提供一个可运行的示例,您可以此为基础,向自己的程序添加相似功能。
 
我的杰作如图 1 所示,该图显示了利用热图来直观展示美国人口的应用程序屏幕快照。该应用程序的构建充分利用了 Windows® Presentation Foundation (WPF) 数据绑定基础结构,这让我能够有效地将应用程序的域特定逻辑和用户界面可视化分离开来。

图 1 人口地图应用程序 (单击该图像获得较小视图)

图 1 人口地图应用程序 (单击该图像获得较大视图)
下一部分中,我会先简单介绍一些初步的 WPF 数据绑定主题,然后我会用本文的剩余篇幅详细说明如何编写该应用程序。我会先介绍应用程序的 Visual Basic 对象模型,然后讨论如何使用 Visual Basic 9.0 中创新性的新 XML 功能和 LINQ 来实现数据处理逻辑。最后我将展示如何使用 WPF 有效地实现应用程序数据可视化。
WPF 数据绑定
WPF 数据绑定基础结构基于依赖对象和依赖属性概念。依赖对象提供对更改通知及动态提取和检索属性值功能的支持。依赖属性则是与依赖对象关联的属性。两者共同构成了 WPF 众多方面(包括数据绑定、动画和样式)背后的核心基础结构。
依赖对象由从 DependencyObject 类派生的类型的实例来表示。同样,依赖属性由已注册到 WPF 属性系统的 DependencyProperty 实例来表示。
用于应用程序数据模型时,依赖属性可大大简化代码。它们允许直接实现许多任务,而无需程序员显式操作控件。通过 WPF 声明性数据绑定和样式机制,更改会自动传播到用户界面,而无需自定义逻辑。
这使得 WPF 用户界面底层的代码更易于阅读、编写和维护。它还倡导各层之间更好的分离,从而提升到更简明的应用程序体系结构。最后(也是最重要的),它可让您充分利用诸如 Expression BlendTM 之类的工具,以可视化方式(注重样式元素而非应用程序逻辑)来设计用户界面。
如需使用依赖属性的简单示例,请参见图 2。其中我展示了一部分 MapRegion 类定义。MapRegion 继承自 DependencyObject。其主要用途是表示地图内的地理区域。它声明了两个依赖属性:RegionName 和 IsSelected。依赖属性通过调用 DependencyProperty 类上的共享 Register 方法进行声明,提供了属性名称、其类型和包含该属性的类的类型(此例中为 MapRegion)。之后,返回的依赖属性会分配给类上声明的公共、共享、只读字段。这一过程主要是为了遵循 WPF 约定,将依赖属性的元数据公开为定义它们的类上的共享字段。
然后,依赖属性通过包装属性(调用 DependencyObject 类中定义的 GetValue 和 SetValue 方法)向代码公开,将适当的 DependencyProperty 对象提供为参数。这使得 MapRegion 类的使用者能够像访问其他任何普通的实例属性一样访问依赖属性,同时充分利用 WPF 中丰富的依赖属性系统。
值得一提的是,实际的属性数据并没有直接存储为 MapRegion 类中的字段。实际上,WPF 会管理依赖对象相关联的每个依赖属性的存储,这样通过调用 SetValue 和 GetValue 便可随时使用它们。
还应注意的是,依赖属性只能向依赖对象注册一次。如果尝试使用同一类型的相同名称注册两个依赖属性,则会引发异常。
在我的示例中,我通过使用调用 DependencyProperty.Register 的显式初始化表达式,来声明共享的只读字段,从而确保每个依赖属性只注册一次。这会导致注册过程作为 MapRegion 类静态构造函数的一部分来执行,而公共语言运行库 (CLR) 确保会运行一次(在执行引用 MapRegion 类的代码之前)。
图 3 所示的 XAML 代码显示了依赖属性如何用于将域模型的更改自动传播到应用程序用户界面的示例。它定义了一个样式,当应用于 Polygon 控件后,在其关联的 MapRegion 将 IsSelected 属性设为 true 时,会使多边形呈现为橙色粗边框,否则为黑色细边框。这让驱动应用程序的代码能够轻松地将区域标记为选定,并让用户界面相应作出响应。

应用程序对象模型
WPF 数据绑定是相当出色的,因此我着手将它尽可能地用于我的应用程序。我的目的是能够编写清晰、简洁和域特定的代码来驱动应用程序,并能够使用 Expression Blend 来设计其界面。
为实现这一点,我创建了可充分利用 WPF 依赖属性系统的对象模型来表示地图信息。该模型由三个主要类组成 — Maps、MapRegion 和 MapPolygon — 全部从 DependencyObject 派生而来。Map 类是对象模型的根,用于表示包含区域列表的地图。MapRegion 类则表示地图上的区域或地理实体。它包含一个多边形列表,每个多边形均描述定义区域的连续地理范围之一的边界。MapPolygon 类表示区域内的多边形,包含了描述其顶点的 Point 对象集合。
Map 类有五个依赖属性:BoundingBox、ScaleX、ScaleY、TranslateX 和 TranslateY。BoundingBox 属性是 Rect 类的实例,存储了包含地图上所有区域的最小矩形范围。ScaleX、ScaleY、TranslateX 和 TranslateY 属性是为支持地图上的缩放和平移而定义的。这些属性至关重要,因为地图上各点的坐标表示从赤道和本初子午线的交叉点开始的偏移量(以千米为单位)。另一方面,WPF 使用不同的坐标系统,按从窗口左上角开始的偏移量进行定义,以逻辑像素为单位(一个逻辑像素等于 1/96 英寸)。
因此,ScaleX、ScaleY、TranslateX 和 TranslateY 属性用于执行下列操作:
缩放地图以便适应窗口大小。 垂直翻动地图,以便它不会绘制成上下颠倒。(在世界视图中,Y 坐标会随着您向北行进而增加;而在 WPF 视图中,Y 坐标会随着您向窗口底部移动而增加。) 移动地图以便其左上角对应于显示地图的控件的左上角。
 
您还可以使用这些属性来实现用户界面内的缩放和平移功能。例如,将 ScaleX 值和 ScaleY 值乘以 1.5 会使地图放大到 150%,将其 TranslateX 属性加 50 则会使地图视图向西移动 50 千米。值得注意的是,更改这些值不会改变地图中的坐标。实际上,这些值是用于向用户界面传达显示地图所需的操作。
Map 类通过 Regions 属性公开其区域集合。但是,Regions 与类上的其他属性不同,它不是依赖属性,而是类型为 ObservableCollection(of MapRegion) 的只读属性。它没有被声明为依赖属性,那是因为我不需要能够一次分配整个集合。然而,该集合本身是可变的,并且可使用 ObservableCollection 类的 Add 和 Remove 方法在地图上添加或删除区域。同时,由于该集合公开为 ObservableCollection,因此它仍参与 WPF 数据绑定系统。例如,如果 ListBox 与 ObservableCollection 进行了数据绑定,则向该集合添加新的项目便会导致该新项出现在 ListBox 中。同样,从集合删除项目也会导致从 ListBox 中删除它。

Map 类
MapRegion 类定义了两个依赖属性:BoundingBox 和 RegionName。BoundingBox 属性定义包含区域的矩形范围,RegionName 则存储区域名称。该类还定义了两个集合属性:Polygons 和 FipsCodes。Polygons 集合存储定义区域的多边形,FipsCodes 则存储与区域相关的一组数字 FIPS(联邦信息处理标准)代码。该类使用 FIPS 代码,因为在应用程序中使用的多边形数据是从美国人口普查局下载的,它使用 FIPS 代码来唯一标识美国的地理实体。您可以从itl.nist.gov/fipspubs/by-num.htm 下载关于联邦信息处理标准的详细信息。此外,还可以从census.gov/geo/www/cob 访问关于美国人口普查局的公共域 TIGER 地理数据库的技术信息。
MapPolygon 类定义了两个属性。第一个是名为 Region 的只读依赖属性,存储对包含多边形的区域的引用。第二个是名为 Points 的集合属性,存储定义多边形顶点的一组 Point 结构。在图 4 中可以看到该类的完整源代码。
Region 属性作为只读依赖属性公开。这使得 WPF 属性系统能够检测对属性值的更改,但不允许 WPF 修改该属性值。只读依赖属性是通过调用 DependencyProperty 类的 RegisterReadOnly 方法来声明的。与 Register 方法不同,RegisterReadOnly 会返回 DependencyPropertyKey 实例而不是返回 DependencyProperty。
依赖属性键是可实现对只读依赖属性进行修改的对象。这些对于实现类(需要内部更新属性值,并由其他类型检测到更改,但不允许对值进行外部修改)非常有用。因此,DependencyPropertyKey 通常不会由定义它们的类公开。这便是 RegionPropertyKey 字段标记为私有的原因。之后,第二个共享字段 RegionProperty 会用于公开提供它的只读别名。
Region 属性为只读属性,因为它设计为无法从外部进行编辑。实际上,当在 MapRegion 类的区域集合中添加或删除多边形时,它会自动设置和清除值。这是通过以下方式实现的:处理多边形集合的 CollectionChangedEvent 并将适当的 Region 实例传播到受影响的多边形。
事件处理程序的实现如图 5 所示。它通过调用存储于处理程序 NotifyCollectionChangedEventArgs 参数中 NewItems 和 OldItems 集合上的 SetRegion 扩展方法来实现。之后该扩展方法会在目标集合中每一项上调用 MapPolygon 类的 SetRegion 方法。该实例方法已标记为友元,以避免被调用。该扩展方法的实现如下所示:
_Sub SetRegion(ByVal x As IEnumerable(Of MapPolygon), _ByVal r As MapRegion)For Each item In xitem.SetRegion(r)NextEnd Sub
 
与 Map 和 MapRegion 类上的集合属性不同,MapPolygon 类上的 Points 集合不是 ObservableCollection(of T)。而是使用了内置 WPF PointsCollection 类。这与 WPF Polygon 控件用于存储它所含一组顶点的类型相同。通过在数据模型中使用此类,便可实现将 Polygon 控件与 MapPolygon 实例直接进行数据绑定。

导入地图数据
应用程序从存储于磁盘上的 XML 文件加载其地图数据。我通过从美国人口普查局下载的原始 ASCII 制图边界文件创建文件,其中说明了美国每个州和县(及等同范围)的边界。州边界数据用于绘制美国地图,县边界数据则用于实现地图上人口数据可视化。然后我取得下载的数据,并将其转换为 XML 文件以便处理。本文的下载提供了将文件转换为 XML 的代码。可从census.gov/geo/www/cob/bdy_files.html 下载原始数据文件。
使用 Visual Studio Create Schema 功能,我便能够自动生成描述 XML 文件内容的 XML 架构。这样我便能够使用新的 Visual Basic XML IntelliSense® 功能,它可依据项目中包括的任何架构文件显示 XML 成员访问表达式(参见图 6)。

图 6 Visual Basic XML IntelliSense (单击该图像获得较小视图)

图 6 Visual Basic XML IntelliSense (单击该图像获得较大视图)

地图数据格式
描述 XML 地图文件的架构定义了一种简单格式,其中包含一个根 File 标记,它随之又包含一系列 Region 标记。每个 Region 标记都包含一系列 FipsCode 和 Polygon 标记,每个 Polygon 标记则包含一系列 Vertex 和 Island 标记。
Region 标记用于描述简单的区域。它定义了两个必需的属性:Type 和 Name。Type 属性提供区域类型的描述。就我的应用程序而言,我通常使用值 State 或 County。但是,在一般情况下,所有值均可用于 Type 属性。Name 属性则说明区域的名称。
区域中定义的每个 FipsCode 标记均说明其中一个 FIPS 代码项。Region 内的最后一个 FipsCode 标记定义了区域的数字 ID。其他 FipsCode 标记则递归描述区域各父项的数字 ID。
就我的应用程序而言,所有区域最多具有两个相关的 FipsCode 值。特别是,描述州(或者州等同范围)的区域通常具有一个项,描述与该州相关的 FIPS 代码。描述县(或县等同范围)的 Regions 通常具有两个项,第一项表示包含县的州 FIPS 代码,第二项则表示县本身的 FIPS 代码。任何特定区域的数字 ID 通常都保证在其直接父项的范围内唯一。
区域的每个 Polygon 标记描述了区域内包含的单个多边形。多边形由一系列 Vertex 标记组成,每个标记描述了单个顶点。Vertex 标记定义了五个必需的属性:Ordinal、Longitude、Latitude、X 和 Y。Ordinal 属性定义了多边形顶点的顺序。其主要目的是,当顶点由不保留顺序的查询处理时,能够对顶点进行正确排序。它目前尚未用于我的应用程序。Longitude 和 Latitude 属性描述了顶点的经度和纬度坐标。同样,X 和 Y 属性定义了投影到矩形空间的顶点坐标和应用程序用于绘制地图的对象。
X 和 Y 属性是通过图 7 所示的简单投影来计算的。此投影不是特别准确,可能不适用于某些情形。在这些情况下,可以使用 Longitude 和 Latitude 而非存储于文件中的 X 和 Y 值来直接计算新值。就我的应用程序而言,并不需要高度的地理精确度,这便已足够。

图 7 极其简单的地图投影
多边形内的 Island 标记定义了包含在其中但不属于它所含区域的空洞。它包含了描述空洞边界的 Vertex 标记的集合。我主要出于完整性目的将它们包含在数据文件中;但并没有用于我的应用程序。这意味着实现地图数据可视化时,地图上会有极小的区域显示稍有错误的值。对于我的应用程序而言,这不是个问题,因此我便忽略而过了。
如果您的应用程序在这方面的准确性至关重要,或者是多边形岛很普遍的映射区域,则只需控制地图的呈现顺序便能轻松解决这一问题。首先绘制包含岛的多边形,然后绘制上部的岛,这样便可以准确呈现多边形岛。在很多情况下,这可能需要额外的处理工作,以便确定重叠区域并构造出适当的顺序。

导入数据
使用 LINQ 将地图数据导入应用程序比较简单。执行这一操作的代码如图 8 所示。它定义了接受以下两个参数的过程 LoadFile:file 和 list。file 参数是一个字符串,包含从磁盘加载 XML 文件的完整路径。list 参数则是对 MapRegion 实例集合的引用。该过程会打开 XML 文件进行处理,并将其定义的所有区域插入到提供的集合。
LoadFile 主体极其简单。此过程的工作方式如下:首先将提供的文件加载到 XDocument 实例,定义查询以遍历文档并将其转换为 MapRegion 实例集合,最后执行查询并将其结果插入提供的输出集合。
该查询由多个不同的片断组成。第一个片断是 From 子句,使用 XML 成员访问表达式 doc.... 来检索文档中所有的多边形 Vertex 标记,作为查询的基础。值得注意的是,它并没有使用 doc...,该表达式可以获取文档中所有的 Vertex 标记(包括 Island 标记内部所定义的)。实际上,它只包括 Polygon 标记内部直接定义的 Vertex 标记。查询的第二部分是 Let 子句。此子句引入了两个新变量,Polygon 和 Region,它们已分配给包含每个顶点的 Polygon 和 Region 标记。
查询的第三部分是首个 Group By 子句。它能够轻松地将顶点列表按其包含的多边形进行分组,然后计算多边形的边界框。包含多边形的区域也包含在分组键中,使它能够用于未来的查询。多边形的边界框是通过计算其顶点 X 和 Y 属性的最小和最大值来计算的。这通过调用自定义聚合函数 MinDBL 和 MaxDBL 来完成,两者由两个扩展方法所定义:
_Function MinDBL(Of T)(ByVal x As IEnumerable(Of T), _ByVal y As Func(Of T, String)) As DoubleReturn x.Min(Function(z) CDbl(y(z)))End Function _Function MaxDBL(Of T)(ByVal x As IEnumerable(Of T), _ByVal y As Func(Of T, String)) As DoubleReturn x.Max(Function(z) CDbl(y(z)))End Function
 
这些方法用于定义聚合函数,聚合函数首先将提供的参数转换为 Double 类型,再根据它计算最小或最大值。
Group By 子句之后是 Select 子句,可按结果将原始分组转换为更好用的格式。特别是,它定义了一个子查询,使用 Group By 产生的分组,只从中投影出顶点,然后使用 ToMapPolygon 扩展方法将产生的集合转换为 MapPolygon 类的实例:
_Function ToMapPolygon(ByVal items As IEnumerable(Of XElement)) As _MapPolygonDim ret As New MapPolygonret.Points.AddRange(From item In items Select item.ToPoint())Return retEnd Function _Function ToPoint(ByVal x As XElement) As PointReturn New Point(x.@X, x.@Y)End Function
 
它还会将原始 MinX、MinY、MaxX 和 MaxY 变量转换为 Rect 类的实例。
查询的下一部分是它的第二个 Group By 子句,可聚合先前根据 Region 选择的结果,并计算包含所有区域多边形的边界框。边界框是通过 Enclose 自定义聚合函数计算的,该函数定义如图 9 所示。查询的最后一部分是最终的 Select 子句,可为从第二个 Group By 返回的每个结果构造 MapRegion 实例。它通过两个子查询来完成这项工作,其中一个可投影出存储于每个 Group 成员中的多边形实例,另一个则提取存储于 Region 中的一组 FIPS 代码。

绘制地图
地图加载到内存后,将它显示在窗口中便轻而易举。图 10 中的 XAML 代码显示了完成此工作的简便方法。此处我定义了一个简单的 Canvas 控件,包含其 Render Transform 的两种转换,以及名为 MapViewer 的单个 ItemsControl 作为其内容。这些转换用于转换地图中区域的坐标,以便它们能正确显示在画布中。ItemsControl 用于以可视方式呈现地图的内容。所有数据绑定都通过使用隐式数据环境来完成。就 MasterCanvas 控件而言,这通常设为 Map 类的实例。
TranslateTransform 将其 TranslateX 和 TranslateY 属性绑定到 Map 类的等效属性。通常,这些值在 Map 类上设为 Map 边界框左上角的反值。这具有移动地图内容的效果,以便其左上角与画布的左上角相对应。同样,ScaleTransform 将其 ScaleX 和 ScaleY 属性绑定到 Map 类上相关的属性。在大多数情况下,它们设为画布宽度与地图宽度之正比,以及画布高度与地图高度之反比。
以下代码显示了 Map 类中 ScaleTo 方法的实现:
Public Sub ScaleTo(ByVal size As Size)If BoundingBox.Width <> 0 AndAlso BoundingBox.Height <> 0 ThenScaleX = size.Width / BoundingBox.WidthScaleY = -size.Height / BoundingBox.HeightTranslateX = -BoundingBox.LeftTranslateY = -BoundingBox.BottomEnd IfEnd Sub
 
给定包含 MasterCanvas 高度和宽度的 Size 结构时,该方法会转换地图,以便在画布上完整显示地图。调用此方法是为了响应画布的 SizeChanged 事件,以便画布所含窗口重新调整大小时,地图能够缩放以填充可用空间:
Private Sub WindowSizeChanged() Handles Me.SizeChangedIf m_Map IsNot Nothing Thenm_Map.ScaleTo(New Size(MasterCanvas.ActualWidth,MasterCanvas.ActualHeight))End IfEnd Sub
 
值得注意的是,所含 TransformGroup 内转换的顺序相当重要。特别是,TranslateTransformation 必须在 ScaleTransform 之前发生。这允许两个转换的参数可依据域模型(而非用户界面)指定。如果先列出 ScaleTransform,则 TranslateTransform 的值可能需要以像素(而非千米)来指定,这需要显式转换。首先指定 TranslateTransform 可大大简化应用程序的对象模型。
ItemsControl 用于将地图内容呈现到画布上。它的行为类似于 ListBox 控件。实际上,ItemsControl 是 ListBox 的基类,定义了大多数的数据绑定行为。但是,与 ListBox 不同的是,ItemsControl 不会将项目呈现为列表形式。这使得它更适用于呈现地图,因为地图显然不应呈现为列表形式。
ItemsControl 将其 ItemsSource 属性绑定到地图的 Regions 集合,并使用 SimpleCanvasTemplate 和 RegionTemplate 资源来定义如何呈现所有内容。用于 ItemsControl 之 ItemsPanel 属性的 SimpleCanvasTemplate 仅仅创建一个空 Canvas。它的目的是定义容器,用于承载由控件的 ItemTemplate 创建的每个项。使用画布的主要原因是画布允许显式定位其内容。其他“容器控件”(如 StackPanel)会尝试应用最终无法正确绘制地图的自动布局逻辑。
RegionTemplate 资源定义了用于地图中每个 Region 的可视化布局。其工作方式是为每个区域声明一个嵌套 ItemsControl,并将其 ItemSource 绑定到区域的 Polygon 集合即可。与 MapViewer 一样,RegionTemplate 中定义的嵌套 ItemsControl 使用画布作为其 ItemsPanel(实际上它重用了 SimpleCanvasTemplate)。但是,与 MapViewer 不同的是,它使用 PolygonTemplate 资源来呈现其项目。
PolygonTemplate 资源定义了一个数据模板,可呈现给定 MapPolygon 类的实例的 WPF 多边形控件。它将每个 Polygon 的 Points 集合绑定到 MapPolygon 实例的 Points 属性。值得一提的是,Polygon 控件将其 Fill 属性显式设为 Transparent。这对于使 Polygon 能响应多个鼠标事件是不可或缺的。WPF 会区分设置了 Fill 属性和没有设置此属性的多边形。缺少 Fill 画笔的多边形被视为外壳多边形,而显式设置了 Fill 画笔的多边形则认为是已填充的多边形。用于已填充多边形的鼠标事件(如 MouseUp、MouseEnter 和 MouseLeave)会在鼠标位于多边形的内部时激发。就外壳多边形而言,图形的内部并不视为其定义的一部分。在此情况下,将鼠标置于多边形的内部不会激发任何鼠标事件。但是,通过将多边形的填充内容设为透明便能够与鼠标交互,而无需遮蔽地图上绘制的其他数据。
多边形模板的另一个有趣方面是,它使用了声明性、基于触发器的样式来驱动多边形外观。特别是,它引用了定义两个截然不同的样式来呈现多边形的 RegionPolygonStyle 资源。区域中 IsSelected 值为 true 的多边形通过橙色粗边框来呈现,IsSelected 值为 false 的则通过黑色细边框来呈现。以下事件处理代码显示了如何轻松地用样式来实现非常酷的用户界面功能,如输入跟踪:
Private Sub Polygon_MouseEnter(ByVal sender As Polygon, ByVal e As _System.Windows.Input.MouseEventArgs)CType(sender.DataContext, MapPolygon).Region.IsSelected = TrueEnd SubPrivate Sub Polygon_MouseLeave(ByVal sender As Polygon, ByVal e As _System.Windows.Input.MouseEventArgs)CType(sender.DataContext, MapPolygon).Region.IsSelected = FalseEnd Sub
 
图 11 显示了使用中的一个示例。

图 11 使用鼠标事件触发器突出显示区域 (单击该图像获得较小视图)

图 11 使用鼠标事件触发器突出显示区域 (单击该图像获得较大视图)

实现人口数据可视化
实现地图上方的人口数据可视化相当简单。实现这一过程的 XAML 代码如图 12 所示。它通过将第二个名为 DataLayer 的 ItemsControl 添加到 MasterCanvas,从而修改用于绘制地图的 XAML。由于 DataLayer 位于 MasterCanvas 内,因此其内容的转换方式与 MapViewer 相同。这允许绑定到 DataLayer 的数据也能够依据底层的域模型(而非用户界面)进行指定。
与 MapViewer 不同的是,DataLayer 并不旨在从 MasterCanvas 直接继承其数据环境,而旨在将要可视化的数据显式设置为其 ItemsSource。提供的数据应该是定义了 Color 和 Points 两个属性的对象集合。Color 属性应为 Brush 类型,并定义由 Points 描述的多边形在地图上应如何着色。Points 属性应为 PointsCollection 的实例,定义要着色的多边形的各顶点。以下是用于呈现 DataLayer 元素的 DataTemplate:

 
它的工作方式是创建多边形并将其 Fill 和 Points 属性绑定到底层元素上的适当属性即可。
图 13 所示的 LoadData 方法负责将主要的地图数据加载到 Map 类的实例,将其设为 MasterCanvas 的 DataContext,构造热图并将 DataLayer 绑定到此。
该方法特别有趣的部分是用于构建热图的查询。它的工作方式是联接地图上显示的一组州、从磁盘加载的一组县以及 XML 人口数据。它将一组州与一组县联接,以便筛选出州中已定义但地图上未显示的县。该联接两边的键便是每个区域的首个 FIPS 代码。就 State 而言,这将对应于其数字 ID。就 County 而言,它对应于包含州数字 ID 的县。XML 人口数据然后与结果联接,以便将每个县的人口值与定义该县的 MapRegion 实例关联起来。
联接的结果会进行筛选,以便包括 2006 年度的人口数据。这是十分有必要的,因为 XML 人口数据包含多个年度的数据,而此案例中,我只想在地图上显示一个特定年度。无法依据年度筛选将会导致在地图为同一区域绘制出具有不同值的多个重叠多边形。筛选的结果会与每个县的 Polygons 集合交叉联接。这可将县集合有效扩展到多边形集合。
查询的最后一部分是 Select 语句,可从每个多边形提取点集合,依据相关人口值计算应显示的颜色,然后构造以该颜色呈现该区域的 SolidColorBrush。这些颜色值是使用 colorMapEntries 数组计算的,该数组定义了人口值和颜色之间的映射。实质上,数组中的每一项都定义了一种颜色和相关的上限人口值。然后从人口所在的两个颜色地图项之间内插用于县的特定颜色。
该内插通过图 14 中定义的 Interpolate 扩展方法来实现。它的工作方式是在 colorMapEntries 数组中执行二分法搜索,查找人口值大于提供值的最小项。然后它取得该项和上一项,并用来内插适当的颜色值。工作方式是调用 ColorMapEntry 类上定义的 Interpolate 实例方法。

结论
我的应用程序的最大优点在于编写简单。特别是,我广泛使用了 Expression Blend 来定义其界面,因此使得添加出色的视觉效果易如反掌。我还使用 XML 来存储应用程序运行所需的所有数据,这使我能够使用 Visual Basic 中的新集成 XML 功能以及 LINQ,轻松实现所有的数据操作逻辑。我还将界面设计为构建于 WPF 数据绑定基础结构之上,因此创建了构建于丰富对象模型之上、清楚分离、结构合理的应用程序。
实质上,通过使用 Visual Basic、WPF、Expression Blend 和 LINQ,我能够快速有效地从现有的数据主体中拼凑出实现相当成熟的可视化的应用程序。此应用程序能够很方便地进行扩展,以便查看不同年度的数据,或以各种方式操作数据。本文的下载提供了所有代码,请自由试验,创造一切可能。

NEW:Explore the sample code online! - or - 代码下载位置:MapsWithVB9WPF2007_12.exe (30813KB)