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 メソッドを実行させる必要があります。
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 使ったサンプルにチャレンジしてみたいと思います。