WPF で Google Maps API を使ってルート検索してみる

昨日も Google Maps API の記事を書きましたが、今日は一歩進めて WPF のWindow 上でルート検索するサンプルを作ってみました。先日から Google Maps API の調査をしてますが、スクリプト書いてるだけじゃ面白くないので、WPF の勉強も兼ねて MVVM パターンを使った少し凝ったサンプルにしています。なんかの役に立てば幸いです。


関連記事Q060. WPF で Google Map API を使うには?


Model

まず Model です。Customer クラスと Customer のコレクションを管理する Customers クラスを用意します。Customer クラスは単純に住所を保持するだけ。Customers は内部で ObservableCollection を使い、Customer のインスタンスを管理します。


Customer.vb

Option Explicit On
Option Strict On

Public Class Customer
	Public Property Address As String
End Class


Customers.vb

Option Explicit On
Option Strict On

Imports System.Collections.ObjectModel
Imports System.ComponentModel

Public Class Customers
	Private _customers As ObservableCollection(Of Customer)
	Default Public Property Items(index As Integer) As ObservableCollection(Of Customer)
		Get
			Return _customers
		End Get
		Set(ByVal value As ObservableCollection(Of Customer))
			_customers = value
		End Set
	End Property
End Class

ViewModel

次は ViewModel です。View と通信するプロパティとコマンドを実装します。WebBrowser に渡す HTML も ViewModel 内で生成しています。ViewModel のコードはどうしても冗長になりますね。おっと、その前に ICommand を実装した RelayCommand クラスを定義しておきましょう。


RelayCommand.vb

Option Explicit On
Option Strict On

Imports System.ComponentModel
Imports System.Windows.Input

Public Class RelayCommand
	Implements ICommand

	Private _canExecuteAction As Func(Of Object, Boolean)
	Private _executeAction As Action(Of Object)

	Public Sub New(ByVal executeAction As Action(Of Object),
				   ByVal canExecuteAction As Func(Of Object, Boolean))
		Me._executeAction = executeAction
		Me._canExecuteAction = canExecuteAction
	End Sub

	Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
		Return _canExecuteAction(parameter)
	End Function

	Public Event CanExecuteChanged(ByVal sender As Object, ByVal e As System.EventArgs) Implements ICommand.CanExecuteChanged

	Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
		_executeAction(parameter)
	End Sub
End Class


ViewModel.vb

Option Explicit On
Option Strict On

Imports System.Collections.ObjectModel
Imports System.ComponentModel

Public Class ViewModel
	Implements INotifyPropertyChanged

	Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged

#Region "コンストラクタ"
	Public Sub New()
		_customers.Add(New Customer() With {.Address = "東京駅"})
	End Sub
#End Region

#Region "プロパティ"
	Private _address As String
	Public Property Address() As String
		Get
			Return _address
		End Get
		Set(ByVal value As String)
			If (_address = value) Then Return
			_address = value
			RaisePropertyChanged("Address")

			Me.AddItem = New RelayCommand(AddressOf AddItemCommand, AddressOf CanAddItem)
		End Set
	End Property

	Private _customer As Customer
	Public Property Customer() As Customer
		Get
			Return _customer
		End Get
		Set(ByVal value As Customer)
			_customer = value
			RaisePropertyChanged("Customer")
		End Set
	End Property

	Private _customers As New ObservableCollection(Of Customer)
	Public Property Customers() As ObservableCollection(Of Customer)
		Get
			Return _customers
		End Get
		Set(ByVal value As ObservableCollection(Of Customer))
			_customers = value
			RaisePropertyChanged("Customers")
		End Set
	End Property

	Public ReadOnly Property NavigateString() As String
		Get
			Return GetNavigateString()
		End Get
	End Property
#End Region

#Region "コマンド"

#Region "AddItem"
	Private _addItem As RelayCommand

	Public Property AddItem() As RelayCommand
		Get
			If _addItem Is Nothing Then
				_addItem = New RelayCommand(AddressOf AddItemCommand, AddressOf CanAddItem)
			End If
			Return _addItem
		End Get
		Set(value As RelayCommand)
			_addItem = value
			RaisePropertyChanged("AddItem")
		End Set
	End Property

	Public Function CanAddItem(ByVal parameter As Object) As Boolean
		If (String.IsNullOrEmpty(Me.Address)) Then
			Return False
		End If
		Return _customers.Count <= 25
	End Function

	Private Sub AddItemCommand(ByVal parameter As Object)
		_customers.Add(New Customer() With {.Address = Me.Address})
		Me.Address = String.Empty
	End Sub
#End Region

#Region "Delete"
	Private _delete As RelayCommand

	Public ReadOnly Property Delete() As RelayCommand
		Get
			If _delete Is Nothing Then
				_delete = New RelayCommand(AddressOf DeleteCommand, AddressOf CanDelete)
			End If
			Return _delete
		End Get
	End Property

	Private Function CanDelete(ByVal parameter As Object) As Boolean
		Return True
	End Function

	Private Sub DeleteCommand(ByVal parameter As Object)
		_customers.Remove(Me.Customer)
	End Sub
#End Region

#End Region

#Region "メソッド"

	Public Function GetNavigateString() As String
		Dim html As New System.Text.StringBuilder()
		With html
			.AppendLine("<!DOCTYPE html '-//W3C//DTD XHTML 1.0 Strict//EN'  ")
			.AppendLine("  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'> ")
			.AppendLine("<!-- saved from url=(0017)http://localhost/ -->")
			.AppendLine("<html xmlns='http://www.w3.org/1999/xhtml'> ")
			.AppendLine("	<head> ")
			.AppendLine("	<meta http-equiv='content-type' content='text/html; charset=utf-8'/> ")
			.AppendLine("	<script type='text/javascript' src='http://maps.google.com/maps?file=api&key=(key)&sensor=false'></script> ")
			.AppendLine("	<script type='text/javascript'> ")
			.AppendLine("		var map; ")
			.AppendLine("		var directions; ")
			.AppendLine()
			.AppendLine("		function initialize() { ")
			.AppendLine("			if (GBrowserIsCompatible()) { ")
			.AppendLine("				map = new GMap2(document.getElementById('map_canvas')); ")
			.AppendLine("				map.enableScrollWheelZoom(); ")
			.AppendLine("				map.setCenter(new GLatLng(35.681379, 139.765577), 13); ")
			.AppendLine("				directions = new GDirections(map, document.getElementById('route')); ")
			.AppendFormat("				var pointArray = [{0}]; ", GetPointList())
			.AppendLine()
			.AppendLine("				directions.loadFromWaypoints(pointArray,{ locale: 'ja_JP' }); ")
			.AppendLine("			} ")
			.AppendLine("		} ")
			.AppendLine("	</script> ")
			.AppendLine("</head> ")
			.AppendLine("	<body onload='initialize()'> ")
			.AppendLine("		<div id='map_canvas' style='width: 100%; height: 400px'></div> ")
			.AppendLine("		<div id='route' style='width: 100%; height: Auto'></div> ")
			.AppendLine("	</body> ")
			.AppendLine("</html> ")
		End With
		Return html.ToString()
	End Function

	Private Function GetPointList() As String
		Dim ret As String = String.Empty
		For Each item In _customers
			ret += "'" + item.Address + "',"
		Next
		If (Not String.IsNullOrEmpty(ret)) Then
			ret = ret.Substring(0, ret.Length - 1)
		End If
		Return ret
	End Function

	Public Sub RaisePropertyChanged(propertyName As String)
		RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
	End Sub

#End Region

End Class

View

最後は View です。WebBrowser は Source プロパティを使えば Uri とバインドできますが ViewModel が生成した HTML を文字列とはバインドできないため、コードビハインドで WebBrowser.NavigateToString メソッドを実行させる必要があります。

MainWindow.xaml.vb

Public Class MainWindow
	Public Shared ReadOnly Navigate As New RoutedCommand

	Public Sub Navigate_Execute(sender As Object, e As RoutedEventArgs)
		If (Me.DataContext Is Nothing) Then Return
		If (Me.browser Is Nothing) Then Return

		Dim navigateString = DirectCast(Me.DataContext, ViewModel).NavigateString
		Me.browser.NavigateToString(navigateString)
	End Sub
End Class


MainWindow.xaml

<Window
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:local="clr-namespace:GoogleMapApiTest"
	xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
	x:Class="MainWindow"
	Title="MainWindow" Height="630" Width="900" 
	WindowStartupLocation="CenterScreen">
	<Window.DataContext>
		<local:ViewModel />
	</Window.DataContext>
	<Window.CommandBindings>
		<CommandBinding Command="local:MainWindow.Navigate" Executed="Navigate_Execute" />
	</Window.CommandBindings>
	<i:Interaction.Triggers>
		<i:EventTrigger EventName="Loaded">
			<i:InvokeCommandAction Command="local:MainWindow.Navigate" />
		</i:EventTrigger>
	</i:Interaction.Triggers>
	<Grid>
		<Grid.RowDefinitions>
			<RowDefinition Height="8" />
			<RowDefinition Height="135" />
			<RowDefinition Height="8" />
			<RowDefinition />
			<RowDefinition Height="8" />
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="8" />
			<ColumnDefinition />
			<ColumnDefinition Width="8" />
		</Grid.ColumnDefinitions>
		<Grid Grid.Column="1" Grid.Row="1" >
			<Grid.ColumnDefinitions>
				<ColumnDefinition />
				<ColumnDefinition Width="8" />
				<ColumnDefinition />
			</Grid.ColumnDefinitions>
			<ListBox Grid.Row="0" Grid.Column="0" DisplayMemberPath="Address"
					 ItemsSource="{Binding Customers}" SelectedValue="{Binding Customer}" >
				<ListBox.InputBindings>
					<KeyBinding Key="Delete" Command="{Binding Delete}" />
				</ListBox.InputBindings>
			</ListBox>
			<StackPanel Grid.Column="2">
				<Label Height="24" Content="ルート検索する住所を入力してください" />
				<TextBox Height="24" VerticalContentAlignment="Center"
						 InputMethod.PreferredImeState="On"
						 InputMethod.PreferredImeConversionMode="FullShape,Native" 
						 Text="{Binding Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
				<Button Width="80" Height="24" Margin="0,12,0,0" 
						Content="追加"
						HorizontalAlignment="Right"
						Command="{Binding AddItem}"/>
				<Button Width="80" Height="24" Margin="0,12,0,0" 
						Content="検索"
						HorizontalAlignment="Right"
						Command="local:MainWindow.Navigate" />
			</StackPanel>
		</Grid>
		<WebBrowser x:Name="browser" Grid.Row="3" Grid.Column="1" />
	</Grid>
</Window>


するとこうなる・・・


次は機会見て、Livet 使ったサンプルにチャレンジしてみたいと思います。