現在位置近くの病院を素早く検索するサンプルプログラム

2013年6月29日(土)
薬師寺 国安

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

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

リスト2 (MainWindow.xaml.vb)

Option Strict On

コンピュータの地理的位置にアクセスできるようにするクラスの含まれる、Windows.Devices.Geolocation名前空間をインポートします。

Imports Windows.Devices.Geolocation

Bing Mapsに関するクラスの含まれる、Bing.Maps名前空間をインポートします。

Imports Bing.Maps

UIに関するクラスの含まれるWindows.UI名前空間をインポートします。PushPinの色を設定する場合等に必要です。

Imports Windows.UI

アプリケーションウィンドウやウインドウ対話を作成し管理するサポートと、ウインドウ上の入力イベントを処理するクラスの含まれる、Windows.UI.Core名前空間をインポートします。

Imports Windows.UI.Core

最新のHTTPアプリケーション用のプログラミング インターフェイスを提供するクラスの含まれる、System.Net.Http名前空間をインポートします。

Imports System.Net.Http

コンテキストメニューおよびメッセージダイアログのサポートを提供するクラスの含まれる、Windows.UI.Popups名前空間をインポートします。

Imports Windows.UI.Popups
  Public NotInheritable Class MainPage
  Inherits Page

現在の地理的位置にアクセスするクラスである、Geolocatorクラスのメンバ変数myGeolocatorを宣言します。

  Dim myGeolocator As Geolocator

Windowsランタイムコアイベントメッセージディスパッチャを提供するクラスである、CoreDispatcherクラスのメンバ変数、myCoreDispacherを宣言します。

  Dim myCoreDispacher As CoreDispatcher

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

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

GoogleのAPI Keyで初期化された、メンバ定数変数GoogleAppIDを宣言します。

  Const GoogleAppID As String = "GoogleのAPI Key"
 
  Dim flag As Boolean = False
  Dim myCoodinatePosition As String = String.Empty
  Dim myAddressCoodinatePosition As String = String.Empty
  Dim myUri As String = String.Empty
  Dim myAddressLatitude As String
  Dim myAddressLongitude As String
  Dim myAddressUri As String

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

場所にアクセスし、場所が変更された時や、Geolocatorの機能が変更された時に発生する各イベントで、イベントハンドラを実行するDataShowプロシージャを実行します。

  Protected Overrides Sub OnNavigatedTo(e As Navigation.NavigationEventArgs)
    DataShow()
  End Sub

場所にアクセスし、場所が変更された時や、Geolocatorの機能が変更された時に発生する各イベントで、イベントハンドラを実行する処理。

myGeolocatorオブジェクトが作成されていない場合は、新しいGeolocatorクラスのインスタンスmyGeolocatorオブジェクトを作成します。
場所認識の精度レベルを表す、DesiredAccuracy プロパティにはDefaultを指定しておきます。
Default以外にHighがありますが、パフォーマンスが低下する恐れがありますので、Defaultを指定しています。

Window.Current.CoreWindow.Dispatcherで、現在アクティブになっている、ウインドウの内部コアオブジェクトのイベントディスパッチャを取得して、myCoreDispacherで参照します。

AddHandlerステートメントで、場所が更新された時のPositionChangedイベント時のイベントハンドラ、myGeolocator_PositionChangedを追加します。

同じく、AddHandlerステートメントで、更新された場所を提供するGeolocatorの機能が変更された時に発生する、StatusChanged時のイベントハンドラ、myGeolocator_StatusChangedを追加します。

  Private Sub DataShow()
    If myGeolocator Is Nothing = True Then
      myGeolocator = New Geolocator
      myGeolocator.DesiredAccuracy = PositionAccuracy.Default
      myCoreDispacher = Window.Current.CoreWindow.Dispatcher
    End If
    AddHandler myGeolocator.PositionChanged, AddressOf myGeolocator_PositionChanged
    AddHandler myGeolocator.StatusChanged, AddressOf myGeolocator_StatusChanged
  End Sub

場所が変更された時の処理

myCoreDispacher.RunAsyncメソッドで、イベントディスパッチャを実行し、ディスパッチされたイベントの結果を非同期に返します。ディスパッチャの優先順位は標準のNormalを指定しています。

緯度と経度のデータや都市の住所データを含めることができるGeoPositionクラス型の変数psoを宣言し、PositionChangedイベントに関連付けられた場所データを取得します。

Clearメソッドで、地図内を一度クリアしておきます。

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

地理的位置に関連付けされた緯度と経度で初期化された、新しいLocationクラスのインスタンスmyLocationオブジェクトを作成します。Locationクラスは、地図上の場所の標高と座標値を含むクラスです。

ブール型のメンバ変数flagの値で条件分岐を行います。FlagがTrueであった場合、つまり、マウスを右クリックして表示される住所を入力するボックスに住所を入力して、「OK」ボタンがクリックされた場合、ということです。その場合は、Locationを、メンバ変数myAddressLatitudeに格納された緯度の値と、myAddressLongitudeに格納された経度の値で初期化します。

それ以外の場合は、現在位置の緯度と経度で初期化します。

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

ブール型のメンバ変数flagの値で条件分岐を行います。FlagがTrueであった場合、つまり、マウスを右クリックして表示される住所を入力するボックスに住所を入力して、「OK」ボタンがクリックされた場合、ということです。

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

そうでない場合には、latとlonに現在の緯度と経度を指定します。

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

XML要素を表すXElementクラス型変数、xmldocを宣言します。

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

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

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

読み込んだXMLから

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

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

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

ブール型のメンバ変数flagの値で条件分岐を行います。FlagがTrueであった場合、つまり、マウスを右クリックして表示される住所を入力するボックスに住所を入力して、「OK」ボタンがクリックされた場合、ということです。

myAddressTextBlockに変数myAddressと文字列「辺り」を連結して表示します。それ以外の場合は、WatermarkTextBoxに入力された値と文字列「辺り」を連結して表示します。

myStackPanelオブジェクトにAddメソッドで住所の設定された、myAddressTextBlockを追加します。

SetValueメソッドでmyAddressStackPanelのZIndexPropertyに20を指定します。myAddressStackPaneが前面に表示されます。

ブール型のメンバ変数flagの値で条件分岐を行います。FlagがTrueであった場合、つまり、マウスを右クリックして表示される住所を入力するボックスに住所を入力して、「OK」ボタンがクリックされた場合、ということです。

MapLayerクラスのSetPositionメソッドで、マップレイヤー内にmyAddressStackPanelオブジェクトの位置を設定します。

Locationをメンバ変数myAddressLatitudeが格納している緯度とmyAddressLongitudeが格納している経度で初期化します。
MapLayerクラスは、地図上の要素の位置を保持しているマップレイヤーを表すクラスです。それ以外の場合は、Locationを現在位置の緯度と経度で初期化します。

myMapにAddメソッドでmyAddressStackPanelオブジェクトを追加します。
myAddressStackPanelは最初の状態では非表示になっているのでわかりませんが、ピンの位置にStackPanelオブジェクトが表示されることになります。

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

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

ブール型のメンバ変数flagの値で条件分岐を行います。FlagがTrueであった場合、つまり、マウスを右クリックして表示される住所を入力するボックスに住所を入力して、「OK」ボタンがクリックされた場合、ということです。

病院の情報を取得するGoogleのWeb APIのURLを設定します。
locationには緯度と経度をカンマで区切って格納している、メンバ変数myAddressCoodinatePositionの値を指定します。
radiusには範囲(メートル単位)を指定します。ここでは1000を指定して1Kmとしています。
typesには、何を目的に検索するかを指定します。ここではHospitalを指定しています。
このtypesには|(パイプ)で区切って複数の値を指定できます。指定できる値の一覧は下記のURLを参照してください。
→ Google Places API/Supported Place Types

Sensorには場所の要求が位置センサー(GPS など)を使用してデバイスから来たかどうかを示す値を、trueまたはfalseで指定します。ここではtrueを指定しています。
languageにはjaをkeyにはGoogleより取得したAPIKeyを指定します。

「OK」ボタンがクリックされていない場合は、locationに現在位置の緯度と経度をカンマで区切った値を格納している、メンバ変数myCoodinatePositionの値を指定します。

検索結果は最大で20件までの情報を取得して表示します。これらの式を変数myUriに格納しておきます。TitleTextBlockにWatermarkTextBlockに入力された値、または現在位置の住所を格納しているmyAddress変数の値と、文字列「辺り」を連結して表示します。

新しいHttpClientのインスタンスmyHttpClientを作成します。
GetStringAsyncメソッドでGoogleの Web APIの式を格納しているmyUriの内容を読み取り、返ってくるXMLを変数resultHospitalに格納します。
XElement.ParseメソッドでresultHospitalが格納しているXMLの内容を、文字列として読み込みます。

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

変数noの値を1ずつ増加させます。この値は、これから作成する「Webで表示」ボタンの、どのボタンがクリックされたかの判定に使用します。

新しいPushPinクラスのインスタンスhospitalPinオブジェクトを作成します。
背景色にCrimsonを指定し、Textプロパティに1ずつ増加する変数noの値を指定します。
ピンの表面に連番が表示されます。

要素の子要素要素の子要素要素の値を変数myLongitude(経度)に、要素の子要素要素の子要素要素の値を変数myLatitude(緯度)に格納します。
地理的位置に関連付けされた緯度と経度で初期化された、新しいLocationクラスのインスタンスhospitalLocationオブジェクトを作成します。
MapLayerクラスのSetPositionメソッドで、マップレイヤー内に病院のピンの位置を設定します。myMapにAddメソッドで病院の位置にピンを追加します。

新しいStackPanelのインスタンスmyStackPanelオブジェクトを作成します。
Marginに5、背景色にNavyを指定し、非表示としておきます。

新しいTextBlockのインスタンスmyNameTextBlockを作成します。
文字色はRed、Widthに400、文字の回り込みは可、文字サイズは24、パディングは5と指定します。
Textプロパティには、文字列「病院名」と要素の内容を連結して指定します。病院の名前が表示されます。

新しいTextBlockのインスタンスaddressTextBlockを作成します。
文字色はBeige、Widthに400、文字の回り込みは可、文字サイズは20、パディングは5と指定します。
Textプロパティには、文字列「住所」と要素の値を連結して表示します。病院の住所が表示されます。

新しいButtonのインスタンスmyButtonを作成します。
Contentプロパティに「Webで表示」と指定します。
Tagプロパティに変数noの値を指定します。どのButtonがクリックされたかの判定に使用します。

SetValueメソッドで、myStackPanelのZIndexPropertyに変数noの値を指定します。myStackPanelがPushPinよりも前面に表示されるようになります。

myStackPanelオブジェクトにAddメソッドで、myNameTextBlock、addressTextBlock、myButtonを追加します。

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

AddHandlerステートメントで、病院の位置を表すピンがタップされた時のイベントハンドラを追加します。
myStackPanelオブジェクトが表示状態になり、病院の情報が表示されます。

AddHandlerステートメントで、病院の情報を表示した、myStackPanelがタップされた時のイベントハンドラを追加します。myStackPanelオブジェクトが非表示になります。

AddHandlerステートメントで、Buttonがクリックされた時のイベントハンドラを追加します。
バーを表す、クリックされたButtonのTagの値を変数indexに格納します。数値キャストしたindexの値をmyIndexに格納します。-1しているのは、ピンの表面に表示される連番は1~20まで表示されますが、XML要素のインデックスは0から始まるため-1しています。

変数mySendAddressにmyIndexに位置する要素の値と、myIndexに位置する要素の値を半角の空白で区切って格納しておきます。住所+半角の空白+病院名の値が格納されます。
これらの値はUri.EscapeDataStringメソッドでエスケープ表現に変換しておきます。
Windows.System.Launcher.LaunchUriAsyncメソッドで、指定されたURIのURIスキーム名に関連付けられている既定のアプリケーション(この場合はIE10のブラウザ)を起動します。
先にも書きましたが、この記述方法では、ストアの審査には通りません。このようなやり方もある、という認識でお読みください。ストアの審査に通るにはWebBrowserコントロールを使って、その中に表示する方法を採るといいでしょう。

Try~Catch~End Tryで例外処理を行っています。例外が発生した場合はErrorShowプロシージャを実行します。

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

  Private Async Sub myGeolocator_PositionChanged(sender As Geolocator, args As PositionChangedEventArgs)
    Dim myLatitude As String = String.Empty
    Dim myLongitude As String = String.Empty
    Dim no As Integer = 0
    Await myCoreDispacher.RunAsync(CoreDispatcherPriority.Normal, Async Sub()
                  Dim pos As Geoposition = args.Position
                  myMap.Children.Clear()
                  Dim myPin As New Pushpin
                  Dim myLocation As Location
                  myPin.Background = New SolidColorBrush(Colors.Navy)
                  If flag = True Then
                      myLocation = New Location(CDbl(myAddressLatitude), CDbl(myAddressLongitude))
                        Else
                          myLocation = New Location(CDbl(pos.Coordinate.Latitude), CDbl(pos.Coordinate.Longitude))
                        End If
                    MapLayer.SetPosition(myPin, myLocation)
                    myMap.Children.Add(myPin)
                    myMap.SetView(myLocation, 16)

                  If flag = True Then
                    myAddressUri = String.Format("http://reverse.search.olp.yahooapis.jp/OpenLocalPlatform/V1/reverseGeoCoder?lat={0}&lon={1}&appid={2}", myAddressLatitude, myAddressLongitude, AppID)
                  Else
                    myAddressUri = String.Format("http://reverse.search.olp.yahooapis.jp/OpenLocalPlatform/V1/reverseGeoCoder?lat={0}&lon={1}&appid={2}", pos.Coordinate.Latitude, pos.Coordinate.Longitude, AppID)
                  End If
                  Dim myAddressHttpClient As New HttpClient
                  Dim myCurrentAddress As String = String.Empty
                  Dim xmldoc As XElement
                  Try
                    myCurrentAddress = Await myAddressHttpClient.GetStringAsync(myAddressUri)
                    myCurrentAddress = myCurrentAddress.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>")
                    xmldoc = XElement.Parse(myCurrentAddress)
                    Dim myAddress = xmldoc.Descendants("Address").Value
                    Dim myAddressStackPanel As New StackPanel
                    myAddressStackPanel.Margin = New Thickness(5)
                    myAddressStackPanel.Background = New SolidColorBrush(Colors.Navy)
                    myAddressStackPanel.Visibility = Xaml.Visibility.Collapsed
                    Dim myAddressTextBlock As New TextBlock
                    myAddressTextBlock.Foreground = New SolidColorBrush(Colors.Red)
                    myAddressTextBlock.FontSize = 24
                    myAddressTextBlock.Padding = New Thickness(5)
                    If flag = False Then
                      myAddressTextBlock.Text = myAddress & " 辺り"
                        Else
                          myAddressTextBlock.Text = WatermarkTextBox.Text & " 辺り"
                        End If
                    myAddressStackPanel.Children.Add(myAddressTextBlock)
                    myAddressStackPanel.SetValue(Canvas.ZIndexProperty, 20)
                        If flag = False Then
                                MapLayer.SetPosition(myAddressStackPanel, New Location(CDbl(pos.Coordinate.Latitude), CDbl(pos.Coordinate.Longitude)))
                        Else
                               MapLayer.SetPosition(myAddressStackPanel, New Location(CDbl(myAddressLatitude), CDbl(myAddressLongitude)))
                        End If
                    myMap.Children.Add(myAddressStackPanel)
                    AddHandler myPin.Tapped, Sub()
                      myAddressStackPanel.Visibility = Xaml.Visibility.Visible
                    End Sub
 
                    AddHandler myAddressStackPanel.Tapped, Sub()
                      myAddressStackPanel.Visibility = Xaml.Visibility.Collapsed
                    End Sub
 
                          If flag = True Then
                                myUri = String.Format("https://maps.googleapis.com/maps/api/place/nearbysearch/xml?location={0}&radius=1000&types=hospital&sensor=true&language=ja&key={1}", myAddressCoodinatePosition, GoogleAppID) (B)
                            TitleTextBlock.Text = WatermarkTextBox.Text & " 辺りの1Km範囲内の病院を表示"
                          Else
                            myCoodinatePosition = pos.Coordinate.Latitude & "," & pos.Coordinate.Longitude
                            myUri = String.Format("https://maps.googleapis.com/maps/api/place/nearbysearch/xml?location={0}&radius=1000&types=hospital&sensor=true&language=ja&key={1}", myCoodinatePosition, GoogleAppID)
                            TitleTextBlock.Text = myAddress & " 辺りの1Km範囲内の病院を表示"
                          End If
                  Dim myHttpClient As New HttpClient
                  Dim resultHospital = Await myHttpClient.GetStringAsync(myUri)
                Dim httpDoc As XElement = XElement.Parse(resultHospital)
                  For Each result In From c In httpDoc.Descendants("result") Select c
                    no += 1
                    Dim hospitalPin As New Pushpin
                      hospitalPin.Background = New SolidColorBrush(Colors.Crimson)
                      hospitalPin.Text = no.ToString
                      myLongitude = result.Element("geometry").Element("location").Element("lng").Value
             myLatitude = result.Element("geometry").Element("location").Element("lat").Value
                    Dim hospitalLocation = New Location(CDbl(myLatitude), CDbl(myLongitude))
                    MapLayer.SetPosition(hospitalPin, hospitalLocation)
                    myMap.Children.Add(hospitalPin)
                    Dim myStackPanel As New StackPanel
                    myStackPanel.Margin = New Thickness(5)
                    myStackPanel.Background = New SolidColorBrush(Colors.Navy)
                    myStackPanel.Visibility = Xaml.Visibility.Collapsed
                    Dim myNameTextBlock As New TextBlock
                    myNameTextBlock.Foreground = New SolidColorBrush(Colors.Red)
                    myNameTextBlock.Width = 400
                    myNameTextBlock.TextWrapping = TextWrapping.Wrap
                    myNameTextBlock.FontSize = 24
                    myNameTextBlock.Padding = New Thickness(5)
                    myNameTextBlock.Text = "【病院名】=" & result.Element("name").Value
                    Dim addressTextBlock As New TextBlock
                    addressTextBlock.Foreground = New SolidColorBrush(Colors.Beige)
                    addressTextBlock.Width = 400
                    addressTextBlock.TextWrapping = TextWrapping.Wrap
                    addressTextBlock.FontSize = 20
                    addressTextBlock.Padding = New Thickness(5)
                    addressTextBlock.Text = "【住所】=" & result.Element("vicinity").Value
                    Dim myButton As New Button
                      With myButton
                        .Content = "Webで表示"
                        .Tag = no.ToString
                      End With
                    myStackPanel.SetValue(Canvas.ZIndexProperty, no)
                    myStackPanel.Children.Add(myNameTextBlock)
                    myStackPanel.Children.Add(addressTextBlock)
                    myStackPanel.Children.Add(myButton)
          MapLayer.SetPosition(myStackPanel, New Location(CDbl(myLatitude), CDbl(myLongitude)))
          myMap.Children.Add(myStackPanel)
            AddHandler hospitalPin.Tapped, Sub()
              myStackPanel.Visibility = Xaml.Visibility.Visible
            End Sub
 
            AddHandler myStackPanel.Tapped, Sub()
              myStackPanel.Visibility = Xaml.Visibility.Collapsed
            End Sub
 
            AddHandler myButton.Click, Sub(mySender As Object, myArgs As RoutedEventArgs)
              Dim index = DirectCast(mySender, Button).Tag
              Dim myIndex = CInt(index) - 1
              Dim mySendAddress = "http://www.bing.com/search?q=" & Uri.EscapeDataString(httpDoc.Descendants("vicinity")(myIndex).Value & " " & httpDoc.Descendants("name")(myIndex).Value)
              Await Windows.System.Launcher.LaunchUriAsync(New Uri(mySendAddress))
            End Sub
        Next
      Catch
        ErrorShow()
      End Try
    End Sub)
  End Sub

WatermarkTextBoxに住所を入力して「OK」ボタンをタップした際に、エラーが発生した時の処理

警告メッセージを表示して、現在位置の住所と病院の位置を表示します。

  Private Async Sub ErrorShow()
    Dim message As New MessageDialog("住所が不正です。現在位置を表示します。")
    Await message.ShowAsync
    resetButton_Click(Nothing, Nothing)
  End Sub
  • 現在位置近くの病院を検索するプログラム

薬師寺国安事務所

薬師寺国安事務所代表。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メルマガ会員のサービス内容を見る

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