写真と現在位置を入れた画像日記アプリを作る

2013年8月29日(木)
薬師寺 国安

ページがアクティブになった時の処理

MainPageから送られた値はe.Parameterで取得できます。これはObject型であるため、DirectCastで文字列にキャストして、変数readXmlFileに格納しておきます。

ピクチャライブラリのImageDiaryXMLサブフォルダにアクセスします。
GetFileAsyncメソッドで、readXmlFileに格納されているXMLファイルを取得し、myFileで参照します。
読み込み専用モードで、OpenAsyncメソッドでmyFileを開き、myStream変数で参照します。
UTF-8でエンコードされ、myStreamで初期化された、新しいStreamReaderのインスタンスreaderオブジェクトを作成します。
ReadToEndメソッドでファイルの最後まで読み取り、変数resultXmlに格納します。

XElemet.ParseメソッドでresultXmlの値を文字列として読み込みます。

Descendantsメソッドで、全ての子孫要素に対して、その内容を変数resultに格納しながら、以下の処理を行います。

メンバ変数myImageFileに、要素の値を取得して、パスと拡張子を除いたファイル名に、文字列「.jpg」を連結して格納します。

メンバ変数myLatitudeに要素の値を、myLongitudeにの値を格納します。
メンバ変数myCommentには、要素の値を格納します。
メンバ変数delXmlFileにはmyImageFile変数が格納している値から20文字を取り出し、文字列「.xml」と連結して格納しておきます。
メンバ変数myUriに「Yahoo!ローカルサーチAPI」のアドレスを格納します。引数latにメンバ変数myLatitudeが格納している緯度の値を指定して、lonにメンバ変数myLongitudeが格納している経度の値を指定します。
AppidにはYahooのアプリケーションIDを格納しているメンバ定数変数AppIDを指定します。

新しいHttpClientクラスのインスタンスmyHttpClientオブジェクトを作成します。HttpClientクラスは、URIで識別されるリソースにHTTP要求を送信し、そのリソースからHTTP応答を受信するための基本クラスが含まれています。

GetStringAsyncメソッドでmyUriの結果を文字列として返し、変数resultAddressに格納します。GetStringAsyncメソッドは、指定URIにGET要求を送信し、非同期操作で応答本体を文字列として返すメソッドです。

返される文字列はXML形式になっています。このXMLのルート要素には、名前空間や不要な属性が定義されていて、内容を取得する際の邪魔(-_-;)になりますので、Replace関数で、ルート要素()だけに置換しています。

XElement.Parseメソッドで置換された結果のXMLが格納されている、resultAddressを文字列として読み込みます。

読み込んだXMLから

要素の内容を取得して、変数myAddressに格納しておきます。

ピクチャライブラリのサブフォルダImageDiaryにアクセスします。GetFileAsyncメソッドで、メンバ変数myImageFileに格納されている画像ファイルを取得してmyPictureFileで参照します。
OpenReadAsyncメソッドでmyPictureFileのランダムアクセスストリームを開き、myPictureで参照します。

新しいBitmapImageのインスタンスmyBmpオブジェクトを作成します。
SetSourceメソッドにBitmapSourceのソースイメージにmyPictureを指定しておきます。

Locationをメンバ変数myLatitudeが格納している緯度と、myLongitudeが格納している経度で初期化し、myMapのCenterプロパティに指定します。

新しいPushpinクラスのインスタンスmyPinオブジェクトを作成します。
ピンの背景色をCrimsonに指定します。

新しいStackPanelのインスタンスmyStackPanelオブジェクトを作成します。
Marginプロパティに5を指定して余白を設けます。背景色をNavyに指定します。
このStackPanelオブジェクトのインスタンスを、非表示としておきます。

新しいTextBlockのインスタンスmyTextBlockオブジェクトを作成します。文字色をRedに指定します。
文字サイズに24を指定し、パディングに10を指定します。
Textプロパティに住所を格納している変数myAddressと、文字列「辺り」を連結して指定します。

新しいTextBlockのインスタンスmyCommentTextBlockオブジェクトを作成します。
文字色をGoldに指定します。文字サイズに20を指定し、パディングに5を指定します。Textプロパティにメンバ変数myCommentを指定します。

新しいImageのインスタンスmyImageオブジェクトを作成します。Widthに160、Heightに120を指定し、Sourceプロパティに、先に作成しておいたmyBmpオブジェクトを指定します。

myStackPanelオブジェクトにAddメソッドで住所の設定された、myTextBlockを、画像の指定されたmyImageを、コメントの指定された、myCommentTextBlockを追加します。

Locationを、メンバ変数myLatitudeが格納している緯度と、myLongitudeが格納している経度で初期化します。

MapLayerクラスのSetPositionメソッドで、マップレイヤー内にピンの位置を設定します。
myMapにAddメソッドでピンを追加します。
SetViewメソッドで、指定された中心部に位置、ズーム レベル、方位、およびピッチにマップビューを設定します。この場合myLocationの位置に15レベルでズームインします。

MapLayerクラスのSetPositionメソッドで、マップレイヤー内にmyStackPanelの位置を設定します。
myMapにAddメソッドでmyStackPanelを追加します。

AddHandlerステートメントでピンをタップした時のイベントハンドラを追加します。
イベントハンドラ内では、myStackPanelオブジェクトを表示状態にします。

AddHandlerステートメントでmyStackPanelをタップした時のイベントハンドラを追加します。イベントハンドラ内では、myStackPanelオブジェクトを非表示状態にします。

非同期処理で行われるため、メソッドの先頭にAsyncを追加します。

  Protected Overrides Async Sub OnNavigatedTo(e As Navigation.NavigationEventArgs)
    Dim readXmlFile = DirectCast(e.Parameter, String)
    Dim myStorageFolder As StorageFolder = Windows.Storage.KnownFolders.PicturesLibrary
    Dim myXmlSubFolder = Await myStorageFolder.CreateFolderAsync("ImageDiaryXML", CreationCollisionOption.OpenIfExists)
    Dim myFile = Await myXmlSubFolder.GetFileAsync(readXmlFile)
    Dim resultXml As String
    Using myStream As IRandomAccessStream = Await myFile.OpenAsync(FileAccessMode.Read)
      Using reader As StreamReader = New StreamReader(myStream.AsStream, System.Text.Encoding.UTF8)
        resultXml = reader.ReadToEnd
      End Using
    End Using
      Dim xmldoc As XElement = XElement.Parse(resultXml)
      For Each result In From c In xmldoc.Descendants("Info") Select c
        myImageFile = IO.Path.GetFileNameWithoutExtension(result.Element("ImageName").Value) & ".jpg"
        myLatitude = result.Element("Latitude").Value
        myLongitude = result.Element("Longitude").Value
        myComment = result.Element("Comment").Value
        delXmlFile = myImageFile.Substring(0, 20) & ".xml"
      Next
 
      Dim myUri As String = String.Format("http://reverse.search.olp.yahooapis.jp/OpenLocalPlatform/V1/reverseGeoCoder?lat={0}&lon={1}&appid={2}", myLatitude, myLongitude, AppID)
      Dim myHttpClient As New HttpClient
      Dim resultAddress = Await myHttpClient.GetStringAsync(myUri)
      resultAddress = resultAddress.Replace("<YDF firstResultPosition=" & ChrW(34) & "1" & ChrW(34) & " totalResultsAvailable=" & ChrW(34) & "1" & ChrW(34) & " totalResultsReturned=" & ChrW(34) & "1" & ChrW(34) & " xmlns=" & ChrW(34) & "http://olp.yahooapis.jp/ydf/1.0" & ChrW(34) & ">", "<YDF>")
      Dim httpDoc As XElement = XElement.Parse(resultAddress)
      Dim myAddress = httpDoc.Descendants("Address").Value
      Dim myFolder = KnownFolders.PicturesLibrary
      Dim mySubFolder = Await myFolder.CreateFolderAsync("ImageDiary", CreationCollisionOption.OpenIfExists)
      Dim myPictureFile = Await mySubFolder.GetFileAsync(myImageFile)
      Dim myPicture = Await myPictureFile.OpenReadAsync
      Dim myBmp As New BitmapImage
      myBmp.SetSource(myPicture)
 
      myMap.Center = New Location(CDbl(myLatitude), CDbl(myLongitude))
 
      Dim myPin As New Pushpin
      myPin.Background = New SolidColorBrush(Colors.Crimson)
      Dim myStackPanel As New StackPanel
      myStackPanel.Margin = New Thickness(5)
      myStackPanel.Background = New SolidColorBrush(Colors.Navy)
      myStackPanel.Visibility = Xaml.Visibility.Collapsed
      
      Dim myTextBlock As New TextBlock
      myTextBlock.Foreground = New SolidColorBrush(Colors.Red)
      myTextBlock.FontSize = 24
      myTextBlock.Padding = New Thickness(10)
      myTextBlock.Text = myAddress & " 辺り"
 
      Dim myCommentTextBlock As New TextBlock
      myCommentTextBlock.Foreground = New SolidColorBrush(Colors.Gold)
      myCommentTextBlock.FontSize = 20
      myCommentTextBlock.Text = myComment
      myCommentTextBlock.Padding = New Thickness(5)
 
      Dim myImage As New Image
      With myImage
        .Width = 160
        .Height = 120
        .Source = myBmp
      End With
 
      myStackPanel.Children.Add(myTextBlock)
      myStackPanel.Children.Add(myImage)
      myStackPanel.Children.Add(myCommentTextBlock)
 
      Dim myLocation = New Location(CDbl(myLatitude), CDbl(myLongitude))
      MapLayer.SetPosition(myPin, myLocation)
      myMap.Children.Add(myPin)
      myMap.SetView(myLocation, 15)
 
      MapLayer.SetPosition(myStackPanel, New Location(CDbl(myLatitude), CDbl(myLongitude)))
      myMap.Children.Add(myStackPanel)
 
      AddHandler myPin.Tapped, Sub()
                               myStackPanel.Visibility = Xaml.Visibility.Visible
                           End Sub
 
      AddHandler myStackPanel.Tapped, Sub()
                               myStackPanel.Visibility = Xaml.Visibility.Collapsed
                                  End Sub
    End Sub

「Delete」アイコンがタップされた時の処理

ピクチャライブラリのサブフォルダImageDiaryXMLにアクセスします。

GetFileAsyncメソッドで、メンバ変数myImageFileの格納している画像を取得して、変数myDelImageFileで参照します。
DeleteAsyncメソッドで、画像ファイルを削除します。

GetFileAsyncメソッドで、メンバ変数delXmlが格納しているXMLファイルを、変数myDelXmlFileで参照します。

DeleteAsyncメソッドでXMLファイルを削除します。

削除した旨のメッセージを表示し、myMap内をクリアします。

非同期処理で行われるため、メソッドの先頭にAsyncを追加します。

  Private Async Sub delButton_Click(sender As Object, e As RoutedEventArgs) Handles delButton.Click
    Dim myStorageFolder As StorageFolder = Windows.Storage.KnownFolders.PicturesLibrary
    Dim myXmlSubFolder = Await myStorageFolder.CreateFolderAsync("ImageDiaryXML", CreationCollisionOption.OpenIfExists)
    Dim delImageSubFolder = Await myStorageFolder.CreateFolderAsync("ImageDiary", CreationCollisionOption.OpenIfExists)
    Dim myDelImageFile = Await delImageSubFolder.GetFileAsync(myImageFile)
    Await myDelImageFile.DeleteAsync()
    Dim myDelXmlFile = Await myXmlSubFolder.GetFileAsync(delXmlFile)
    Await myDelXmlFile.DeleteAsync()
 
    Dim message As New MessageDialog("削除しました!")
    Await message.ShowAsync
    myMap.Children.Clear()
  End Sub

次に、ソリューションエクスプローラー内のDataIchiranShowPage.xamlを展開して表示される、DataIchiranShowPage.xaml.vbをダブルクリックしてリスト6のコードを記述します。

ロジックコードを記述する

リスト6 (DataIchiranShowPage.xaml.vb)

Option Strict On
Imports Windows.Storage
Imports Windows.Storage.Streams
Imports System.Net.Http
Imports Windows.UI
Imports Windows.UI.Popups

Public NotInheritable Class DataIchiranShowPage
  Inherits Page

「YahooのアプリケーションID」で初期化された、文字列型の定数メンバ変数を宣言します。

Const AppID As String = "YahooのアプリケーションID"

文字列型の新しいリストである、dateListメンバ変数を宣言します。

  Dim dateList As New List(Of String)

ページがアクティブになった時の処理

ピクチャライブラリのImageDiaryXMLサブフォルダにアクセスします。
GetFileAsyncメソッドで、ImageDiaryXMLサブフォルダ内に格納されているXMLファイルを取得し、コレクション変数myFileに格納します。

コレクション変数myFile内を変数resultで反復処理しながら、以下の処理を行います。

OpenReadAsyncメソッドでmyFileコレクション変数内のファイルを読み込み、変数myStreamで参照します。

UTF-8でエンコードされ、myStreamで初期化された、新しいStreamReaderのインスタンスreaderオブジェクトを作成します。ReadToEndメソッドでファイルの最後まで読み取り、変数resultXmlに格納します。

XElemet.ParseメソッドでresultXmlの値を文字列として読み込みます。

Descendantsメソッドで、全ての子孫要素に対して、その内容を変数myContentsに格納しながら、以下の処理を行います。

メンバ変数myImageFileに、要素の値を取得して、パスと拡張子を除いたファイル名に、文字列「.jpg」を連結して格納します。

メンバ変数myLatitudeに要素の値を、myLongitudeにの値を格納します。

メンバ変数myCommentには、要素の値を格納します。

ピクチャライブラリ内のImageDiaryサブフォルダにアクセスします。GetFileAsyncメソッドで、変数myImageFileが格納しているJPG画像を取得し、変数myPictureFileで参照します。
myPictureFileをOpenReadAsyncメソッドで、ランダムアクセスストリームを開き、myPictureで参照します。

新しいBitmapImageのインスタンスmyBmpオブジェクトを作成し、SetSourceメソッドで、myPictureオブジェクトをソースイメージに指定します。

変数myUriに「Yahoo!ローカルサーチAPI」のアドレスを格納します。
引数latに変数myLatitudeが格納している緯度の値を指定して、lonに変数myLongitudeが格納している経度の値を指定します。
AppidにはYahooのアプリケーションIDを格納しているメンバ定数変数AppIDを指定します。

新しいHttpClientクラスのインスタンスmyHttpClientオブジェクトを作成します。HttpClientクラスは、URIで識別されるリソースにHTTP要求を送信し、そのリソースからHTTP応答を受信するための基本クラスが含まれています。

GetStringAsyncメソッドでmyUriの結果を文字列として返し、変数resultAddressに格納します。GetStringAsyncメソッドは、指定URIにGET要求を送信し、非同期操作で応答本体を文字列として返すメソッドです。
返される文字列はXML形式になっています。このXMLのルート要素には、名前空間や不要な属性が定義されていて、内容を取得する際の邪魔(-_-;)になりますので、Replace関数で、ルート要素()だけに置換しています。

XElement.Parseメソッドで置換された結果のXMLが格納されている、resultAddressを文字列として読み込みます。

読み込んだXMLから

要素の内容を取得して、変数myAddressに格納しておきます。

コレクション変数myFile内のXMLファイルの、完全なファイルシステムパスをResultプロパティで取得し、IO.Path.GetFileNameWithoutExtensionメソッドで、パスと拡張子を除いたファイル名を取得し、変数myDateに格納します。
dateListメンバ変数に、変数myDateから11文字分取りだした値を、Addメソッドで追加します。2013年01月01日という11文字の文字列が追加されます。

新しいTextBlockのインスタンスmyDateTextBlockを作成します。文字色にCrimson、文字サイズに28、Textプロパティに変数myDateの値を指定します。

新しいTextBlockのインスタンスmyAddressTextBlockを作成します。文字色にNavy、文字サイズに20、文字の回り込みを可、Textプロパティに変数myAddressの値と文字列「辺り」を連結して指定します。
新しいImageのインスタンスmyImageオブジェクトを作成します。Widthに384、Heightに288、SourceプロパティにmyBmpオブジェクトを指定します。

新しいListBoxのインスタンスmyListBoxオブジェクトを作成します。WidthとHeightを指定し、AddメソッドでmyComment変数の値を追加します。

新しいStackPanelのインスタンスmyStackPanelオブジェクトを作成します。Widthを指定し、背景色にPink、Marginに10を指定して余白を設けます。

AddメソッドでmyStackPanelオブジェクトに、myDateTextBlock、myAddressTextBlock、myImage、myListBoxオブジェクトを追加します。

最後に、myStackPanelオブジェクトをGridViewに追加します。これで、日付、住所、画像、コメント付きのStackPanelがGridView内に一覧で表示されます。

非同期処理で行われるためメソッドの先頭にAsyncを追加します。

  Protected Overrides Async Sub OnNavigatedTo(e As Navigation.NavigationEventArgs)
    Dim xmlFolder As StorageFolder = KnownFolders.PicturesLibrary
    Dim myXmlSubFolder As StorageFolder = Await xmlFolder.CreateFolderAsync("ImageDiaryXML", CreationCollisionOption.OpenIfExists)
 
    Dim myFile = Await myXmlSubFolder.GetFilesAsync
    Dim resultXml As String
      For Each result In myFile
        Using myStream As IRandomAccessStream = Await result.OpenReadAsync
          Using reader As StreamReader = New StreamReader(myStream.AsStream, System.Text.Encoding.UTF8)
            resultXml = reader.ReadToEnd
            Dim xmldoc As XElement = XElement.Parse(resultXml)
            For Each myContents In From c In xmldoc.Descendants("Info") Select c
              Dim myImageFile = IO.Path.GetFileNameWithoutExtension(myContents.Element("ImageName").Value) & ".jpg"
              Dim myLatitude = myContents.Element("Latitude").Value
              Dim myLongitude = myContents.Element("Longitude").Value
              Dim myComment = myContents.Element("Comment").Value
 
              Dim mySubFolder = Await xmlFolder.CreateFolderAsync("ImageDiary", CreationCollisionOption.OpenIfExists)
              Dim myPictureFile = Await mySubFolder.GetFileAsync(myImageFile)
              Dim myPicture = Await myPictureFile.OpenReadAsync
              Dim myBmp As New BitmapImage
              myBmp.SetSource(myPicture)
 
              Dim myUri As String = String.Format("http://reverse.search.olp.yahooapis.jp/OpenLocalPlatform/V1/reverseGeoCoder?lat={0}&lon={1}&appid={2}", myLatitude, myLongitude, AppID)
              Dim myHttpClient As New HttpClient
              Dim resultAddress = Await myHttpClient.GetStringAsync(myUri)
              resultAddress = resultAddress.Replace("<YDF firstResultPosition=" & ChrW(34) & "1" & ChrW(34) & " totalResultsAvailable=" & ChrW(34) & "1" & ChrW(34) & " totalResultsReturned=" & ChrW(34) & "1" & ChrW(34) & " xmlns=" & ChrW(34) & "http://olp.yahooapis.jp/ydf/1.0" & ChrW(34) & ">", "<YDF>")
 
              Dim httpDoc As XElement = XElement.Parse(resultAddress)
              Dim myAddress = httpDoc.Descendants("Address").Value
 
              Dim myDate = IO.Path.GetFileNameWithoutExtension(result.Path)
              dateList.Add(myDate.Substring(0, 11))
 
              Dim myDateTextBlock As New TextBlock
              With myDateTextBlock
                .Foreground = New SolidColorBrush(Colors.Crimson)
                .FontSize = 28
                .Text = myDate
              End With
 
              Dim myAddressTextBlock As New TextBlock
              With myAddressTextBlock
                .Width = 384
                .Foreground = New SolidColorBrush(Colors.Navy)
                .FontSize = 20
                .TextWrapping = TextWrapping.Wrap
                .Text = myAddress & "辺り"
              End With
 
              Dim myImage As New Image
              With myImage
                .Width = 384
                .Height = 288
                .Source = myBmp
              End With
 
              Dim myListBox As New ListBox
              myListBox.Width = 384
              myListBox.Height = 150
              myListBox.Items.Add(myComment)
 
              Dim myStackPanel As New StackPanel
              With myStackPanel
                .Width = 384
                .Background = New SolidColorBrush(Colors.Pink)
                .Margin = New Thickness(10)
              End With
 
              myStackPanel.Children.Add(myDateTextBlock)
              myStackPanel.Children.Add(myAddressTextBlock)
              myStackPanel.Children.Add(myImage)
              myStackPanel.Children.Add(myListBox)
              GridView1.Items.Add(myStackPanel)
            Next
          End Using
        End Using
      Next
  End Sub

「Yes」ボタンがタップされた時の処理

日付を入力するsearchTextBoxに何も入力されていなければ、処理を抜けます。日付が入力された時は、日付を格納しているメンバ変数dateListオブジェクトのCountプロパティで、格納されている日付の個数分、変数iで反復処理を行います。
変数iに該当するdateListの値が、searchTextBoxに入力された値と同じなら、ScrollIntoViewメソッドで、指定された項目が表示されるよう、リストをスクロールします。同じ日付が見つかった場合は、その位置を表示し、反復処理を抜けます。

  Private Sub okButton_Click(sender As Object, e As RoutedEventArgs) Handles okButton.Click
    If searchTextBox.Text = String.Empty Then
      Exit Sub
    Else
      GridView1.SelectedIndex = -1
      For i As Integer = 0 To dateList.Count - 1
        If dateList(i) = searchTextBox.Text Then
          GridView1.SelectedIndex = i
          GridView1.ScrollIntoView(GridView1.SelectedItem)
          Exit For
        End If
      Next
    End If
  End Sub
End Class
End Class

アイコンの作成

ソリューションエクスプローラーのAssetsフォルダ内には、4つのpngファイルが入っています(表1)。

表1:Assetsフォルダ内に入っているpngファイルの種類

ファイル名 サイズ
Logo.png 150×150
SmallLogo.png 30×30
SplashScreen.png 620×300
StoreLogo.png 50×50

表1の画像はデフォルトでは、□に×の画像になっています。ストアの審査では、このままの画像では審査に受かりませんので、4種類のアイコンを作る必要があります。このサンプルは実際にストアで審査の通ったアプリですので、Assetsフォルダ内には筆者の作成したアイコンが収められていますので、見てみてください。

SplashScreen.pngはアプリを起動した際に、一瞬最初に表示される画像です。スタート画面にピン止めされる画像はデフォルトでは150×150のLogo.pngが使用されます。これを長方形の画像にしたい場合は、310×150サイズのpng画像を作成し、WideLogo.png(任意の名前でOK)としてAssetsフォルダに追加しておきます。
詳細については、「自分の現在位置を取得して表示するサンプルプログラム」の記事を参照してください。

今回はここまでです。ありがとうございました。

薬師寺国安事務所

薬師寺国安事務所代表。Visual Basic プログラミングと、マイクロソフト系の技術をテーマとした、書籍や記事の執筆を行う。
1950年生まれ。事務系のサラリーマンだった40歳から趣味でプログラミングを始め、1996年より独学でActiveXに取り組む。1997年に薬師寺聖とコラボレーション・ユニット PROJECT KySS を結成。2003年よりフリーになり、PROJECT KySS の活動に本格的に参加、.NETやRIAに関する書籍や記事を多数執筆する傍ら、受託案件のプログラミングも手掛ける。Windows Phoneアプリ開発を経て、現在はWindows ストア アプリを多数公開中

Microsoft MVP for Development Platforms - Client App Dev (Oct 2003-Sep 2012)。Microsoft MVP for Development Platforms - Windows Phone Development(Oct 2012-Sep 2013)。Microsoft MVP for Development Platforms - Client Development(Oct 2013-Sep 2014)。Microsoft MVP for Development Platforms-Windows Platform Development (Oct 2014-Sep 2015)。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています