データを地図に表示するアプリをつくろうと思うと、Google MapやBing Mapを使うWebアプリとして作成するのが手軽ですが、ComponentOne Studioの地図コンポーネントを使えば同じような手軽さで地図対応のWPFアプリが作成できます。
WPFアプリの下準備
プロジェクト構成の準備
今回作成するWPFアプリはModel、ViewModel、Viewのクラス構成をとり、Modelにロジック、Viewに画面デザイン、その両者をViewModelで接続することで画面定義とロジックコードの結合を疎にしてそれぞれの役目が分かりやすくまとまるように考慮しています。
そのため、WPFアプリのプロジェクトを生成した直後の構成から一度「MainWindow.xaml」を削除し、Modelsフォルダ、ViewModelsフォルダ、Viewsフォルダを作成して、それぞれのフォルダに、AEDModelクラス、MainViewModelクラス、MainWindow.xamlを配置します。
追加ライブラリの準備
今回使用するオープンデータはREST/JSON形式でデータを取得するので、JSONデータの取り扱いが楽になるようにJSON.NETライブラリをNuGetから導入します。
地図コンポーネントの準備
ComponentOne StudioのインストーラーでWPFを選択してインストールしてあれば、ツールボックスのアイテム追加でWPFコンポーネントとしてC1.Mapsが表示されるので、チェックしてツールボックスに追加します。
ComponentOne Studioのトライアル版は、グレープシティのWebページから申し込むことができます。
データ取得ロジックの実装
サンプルアプリで使用するオープンデータは、Microsoft Azure Mobile Service上で動作しているAED検索オープンデータを使用します 。
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports System.Net
Imports System.Runtime.CompilerServices
Imports Newtonsoft.Json
Namespace Models
Public Class AedInfo
Public Property Id As Long
Public Property LocationName As String '/* 場所_地名【名称】*/
Public Property FullAddress As String '/* 住所_住所【住所】*/
Public Property Perfecture As String '/* 構造化住所_都道府県 */
Public Property City As String '/* 構造化住所_市区町村 */
Public Property AddressArea As String '/* 構造化住所_町名 */
Public Property AddressCode As String '/* 住所コード */
Public Property Latitude As Single '/* 緯度経度座標系_緯度 */
Public Property Longitude As Single '/* 緯度経度座標系_経度 */
Public Property FacilityId As String '/* 公共設備_ID */
Public Property FacilityName As String '/* 公共設備_名称 */
Public Property FacilityPlace As String '/* 公共設備_設置場所【設置場所】※受付横とか */
Public Property ScheduleDayType As String '/* 公共設備_利用可能時間【利用可能時間】 */
Public Property PhotoOfAedUrl As String '/* 公共設備_写真URL【写真】 */
Public Property FacilityNote As String '/* 公共設備_補足【補足】 */
Public Property LatLng As Point
End Class
Public Class TCity
Public Property Perfecture As String '/* 構造化住所_都道府県 */
Public Property City As String '/* 構造化住所_市区町村 */
Private _Items = New ObservableCollection(Of AedInfo)
Public Property Items As ObservableCollection(Of AedInfo)
Get
Return Me._Items
End Get
Set(value As ObservableCollection(Of AedInfo))
Me._Items = value
End Set
End Property
End Class
Public Class AedModel
Implements INotifyPropertyChanged
Public Sub New()
End Sub
Private _City = New TCity
Public Property City As TCity
Get
Return Me._City
End Get
Set(value As TCity)
Me._City = value
OnPropertyChanged()
End Set
End Property
Public Async Function SelectData(perfectureName As String, cityName As String) As Task
Dim targetAeds = New ObservableCollection(Of AedInfo)
Try
Dim urlString = String.Format(
"https://aed.azure-mobile.net/api/aedinfo/{0}/{1}/",
perfectureName,
cityName)
Dim request = CType((WebRequest.Create(urlString)), HttpWebRequest)
request.Method = "GET"
request.ContentType = "application/x-www-form-urlencoded"
Try
Dim response = CType((Await request.GetResponseAsync()), HttpWebResponse)
Dim responseDataStream = New System.IO.StreamReader(response.GetResponseStream())
Dim responseResult = responseDataStream.ReadToEnd()
Dim json = JsonConvert.DeserializeObject(Of IEnumerable(Of AedInfo))(responseResult)
Dim aeds = From item In json Order By item.FullAddress
targetAeds = New ObservableCollection(Of AedInfo)
For Each aed In aeds
aed.LatLng = New Point(aed.Longitude, aed.Latitude)
targetAeds.Add(aed)
Next
Catch ex As Exception
OnFaild("NetworkError" + ex.Message)
End Try
Catch ex As Exception
OnFaild("NetworkError" + ex.Message)
Finally
Me.City = New TCity With {.Perfecture = perfectureName,
.City = cityName, .Items = targetAeds}
End Try
End Function
Public Event Faild(sender As Object, e As String)
Protected Sub OnFaild(line As String)
RaiseEvent Faild(Me, line)
End Sub
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
End Namespace
C1Mapsコンポーネントの配置
ツールボックスからC1MapsコンポーネントをWPFウィンドウ上にドラッグ&ドロップします。これだけで必要な参照設定とライセンスファイルのプロジェクトファイルへの追加が行われます。
画面デザイン
サンプル画面の構成は、左に地図、右に一覧表の2カラム構成になっていて、検索ボタンをクリックすると特定市区町村のAED情報を検索表示します。
:
(中略)
:
<Grid Grid.Row="1" Margin="15,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="20,0,20,0">
<Custom:C1Maps x:Name="C1Maps1"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Zoom="13"
Width="480" Height="600">
<Custom:C1Maps.Resources>
<DataTemplate x:Key="PinStyle"> …(2)'
<Custom:C1VectorPlacemark
Fill="Red" Stroke="Red"
LabelPosition="Top"
Foreground="Red"
GeoPoint="{Binding LatLng}"> …(3)
<Custom:C1VectorPlacemark.Geometry> …(4)
<EllipseGeometry RadiusX="2"
RadiusY="2" />
</Custom:C1VectorPlacemark.Geometry>
</Custom:C1VectorPlacemark>
</DataTemplate>
</Custom:C1Maps.Resources>
<Custom:C1Maps.Source>
<Custom:VirtualEarthRoadSource ApplicationId="{x:Null}"/>
</Custom:C1Maps.Source>
<Custom:C1VectorLayer ItemsSource="{Binding City.Items}" …(1)
ItemTemplate="{StaticResource PinStyle}"/> …(2)
</Custom:C1Maps>
</StackPanel>
:
(中略)
:
C1Mapsコンポーネントで緯度経度に合わせてマークを描画するには、
- (1)緯度経度が含まれたコレクションをItemsSourceプロパティにBindingする
- (2)マーク描画用スタイルをItemTemplateに指定する
- (3)マーク描画用スタイルの中で、C1VectorPlacemarkのGeoPointに緯度経度をBindingする
- (4)マーク自体はC1VectorPlacemarkのGeometryで形を指定する
という手順で行います。今回はEllipseGeometryを指定しているので●を描画します。
コードビハインド側でループを回して1点1点位置を指定して描画する必要はありません。
検索イベント時のコードの記述
MainWindow.xamlのコードビハインド側に検索用のロジックを一部記載してますが、検索ボタンのCommandプロパティにViewModelのメソッドをBindingする方法の方がより結合度が疎になります。
Imports C1.WPF.Maps.ImageryService
Namespace Views
Public Class MainWindow
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
' InitializeComponent() 呼び出しの後で初期化を追加します。
Me.DataContext = Application.MainVM
End Sub
Private Async Sub Search_Click(sender As Object, e As RoutedEventArgs)
Await Application.MainVM.GetItems("愛知県", "豊田市")
SetCenterPos()
End Sub
Private Sub SetCenterPos()
Try
Dim centerLoc = New Point(0, 0)
Dim counter = 0
Dim items = Application.MainVM.City.Items
'/* 地図の中心位置表示 */
For Each item In items
Dim loc = New Point(item.Longitude, item.Latitude)
centerLoc.Y = Math.Abs(centerLoc.Y * counter + loc.Y) / (counter + 1)
centerLoc.X = Math.Abs(centerLoc.X * counter + loc.X) / (counter + 1)
counter += 1
Next
Me.C1Maps1.Center = centerLoc
Catch ex As Exception
End Try
End Sub
End Class
End Namespace
あとはMainViewModelクラスを記述すれば出来上がりです。
サンプル実行
なお、ライセンス認証しているのにもかかわらず実行時にトライアル版の表示が出る場合は、Propertiesフォルダがプロジェクトに自動追加されていない可能性があります。その場合は、ソリューションエクスプローラーですべてのファイルを表示してPropertiesフォルダごとライセンスファイルをプロジェクトに追加すれば表示が消えますので、もし、トライアル版表示に悩まされている方は一度確認してみるとよいでしょう。
マーカーのカスタマイズ
C1Mapsコンポーネントでは地図マーカーとして特殊な形を描画したいときは、C1VectorPlacemarkのGeometryにXAMLのパス定義を指定します。今回のサンプルで表示するのはAEDの情報なのでハート形マーカーとして、位置をハートの最下部の頂点で指し示すようにカスタマイズしてみましょう。
パスの作成
地道にパスを作成してもいいのですが、Syncfusion Metro Studio 2を使えば登録されているアイコンからパス定義を生成できるので、今回はこの方法でパスを作成します。
XAMLへの設定
C1VectorPlacemarkにGeometryパラメタを追加して、そこにパス定義を設定します。
:
(中略)
:
<Custom:C1VectorPlacemark
Fill="Red" Stroke="Red"
LabelPosition="Top"
Foreground="Red"
GeoPoint="{Binding LatLng}"
Geometry="M7.4166234,16.197001L13.497334,16.345396 …(中略)…E-06z" />
:
(中略)
:
マーカーの先端位置調整
ハートマークの下部の先端は、Y軸方向最下層でありX軸方向は中心となっています。そこでRenderTransform定義を追加して位置をずらしたいと思います。手作業でXAMLを変更してもいいのですが、このようなときに便利なのがBlendです。
ついでに大きさも縦横0.5倍になるように調整しておきましょう。
:
(中略)
:
<Custom:C1VectorPlacemark
Fill="Red" Stroke="Red"
LabelPosition="Top"
Foreground="Red"
GeoPoint="{Binding LatLng}"
Geometry="M7.4166234,16.197001L13.497334,16.345396 …(中略)…E-06z">
<Custom:C1VectorPlacemark.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
<TranslateTransform Y="-20" X="-10"/>
</TransformGroup>
</Custom:C1VectorPlacemark.RenderTransform>
:
(中略)
:
実行
カスタマイズしたマーカーが想定した位置に表示されるかを確認します。
まとめ
意外にもBing MapのWPF対応は、2012/1/12に提供されたBing Maps Windows Presentation Foundation (WPF) Control, Version 1.0 が最新版のようです。デスクトップアプリとして作成するのであれば、描画速度やストアアプリと同じXAML(利用できるパラメタなどに違いはありますが)で画面定義ができてBindingが使える点から考えても、WindowsフォームではなくWPFだと思います。ストアアプリではなくデスクトップアプリとしてオープンデータを活用するのであれば、ぜひWPFとComponentOne Studioを組み合わせて使いやすいアプリを設計構築してみてはいかがでしょうか。





