CodeZine(コードジン)

特集ページ一覧

Delphiでデバイス機能をこってり使いつつマルチデバイス対応してみる

iOS/Android向けにコンポーネントでGPSや加速度センサーを簡単に使う

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2016/09/15 14:00

 デバイス搭載のセンサーなどの機能を利用したアプリケーションを開発する場合、単一コードでiOSとAndroidの双方に対応するのは困難です。しかし、Delphiのフレームワークを用いれば、コンポーネントがプラットフォームの差異を吸収してくれるので、単一コード開発が実現できます。この記事では、DelphiでGPSや加速度センサーを利用するアプリケーションを、単一コードで開発する方法を紹介します。

はじめに

 スマホアプリならではの便利で革新的な機能を実装しようとすると、センサーなどのデバイス機能を利用したくなります。iOSとAndroidの双方に対応したいと、マルチデバイス対応の開発環境を利用していても、この辺が機能限界の分岐点となり、どうしてもプラットフォーム固有のネイティブツールへとなびき、単一コードによる開発効率化を諦める結果になっているようです。

図1 Delphiの最新バージョン10.1 Berlin

 Delphi(そしてC++Builder)は、デバイス機能をネイティブでサポートしながらも、コンポーネントによってデバイス固有の部分をカプセル化し、共通の操作で各デバイス向けの機能を呼び出せるようになっています。そこで今回、さまざまなデバイス機能をてんこ盛りしたアプリを作成し、どの程度マルチデバイス対応がイケてるのかを検証してみようと思います。

 なお、ここで紹介したアプリは、Webセミナー「Delphi Boot Camp」のDay 4でも詳しく取り上げています。

アプリの概要

 今回作成するアプリには、次のような機能を盛り込んでみました。

  1. 地図表示
    • GPSで取得した位置情報を使って現在位置を表示する
    • 地図表示と衛星画像の切り替えを行う
  2. 現在位置の情報表示
    • 測位された緯度経度の情報を表示する
    • 緯度経度の情報から現在位置の住所を取得して表示する
  3. 加速度センサーを用いた水平器
    • 加速度センサーで取得できる3軸(X、Y、Z)の値のうち、X軸とY軸の値を使って水平器を実現する

 いずれも単体ではあまり実用的ではありませんが、業務アプリでもゲームでも、よく利用したくなる機能です。アプリには、次のようにタブで切り替えられる複数ページがあります。

図2 デバイス機能をてんこ盛りしたアプリ

 ここで紹介するアプリのソースコードは、https://github.com/kazinoue/2016_Delphi_bootcamp_day4に掲載しています。

地図に現在位置を表示する

地図表示にTMapViewを使う

 初めに実装してみるのは地図の表示です。多くのスマホアプリで地図が使われていますが、AndroidとiOSそれぞれで標準的な方法が以下のように違います。

Android Google Maps Android API
iOS Map Kitフレームワーク

 通常なら、別々のコードを記述しなければならないところですが、Delphiの場合、プラットフォームごとの実装をコンポーネントレベルでカプセル化してくれるので、単一コードで済みます。行うべき操作は、TMapViewコンポーネントを画面上に配置するだけです。

 マップ上に現在位置を表示するには、GPSから現在位置を取得しなければなりませんが、これもTLocationSensorというコンポーネントを使うだけです。GPSとのやり取りを、プラットフォームごとに異なるAPIを駆使して行うなんて気が遠くなりますが、これなら簡単です。

図3 TMapViewとTLocationSensorを使う

 配置したTMapViewに対しては、以下のプロパティを設定します。LayerOptionsの UserLocationをTrueにすることで、アプリケーション起動時から指定した座標(この場合は現在位置を表示するようにします)周辺の地図を表示します。

LocationSensor1
プロパティ
Active True
MapView1
プロパティ
LayerOptions.UserLocation True
Zoom 15

TLocationSensorで取得した現在位置を地図にセットする

 TLocationSensorのOnLocationChanedイベントは、現在位置に変化があると発生するイベントです。このイベントに応答するイベントハンドラを設定しておけば、現在位置の情報をパラメータとして取得できます。

 つまり、このイベントハンドラで、TMapViewに現在位置を設定すれば、現在位置の地図を表示できるわけです。TMapViewの表示位置を表すのは、Locationプロパティです。フォーム上に配置したTLocationSensorのOnLocationChangedイベントに、以下のコードを記述します。イベントハンドラは、オブジェクトインスペクタのイベントタブから、目的のイベントを選択してダブルクリックすれば自動生成されます(太字箇所を記述)。

procedure TForm1.LocationSensor1LocationChanged(Sender: TObject;
  const OldLocation, NewLocation: TLocationCoord2D);
begin
  // 地図の現在位置情報を書き換える。
  MapView1.Location := TMapCoordinate.Create(
    NewLocation.Latitude, NewLocation.Longitude );
end;

地図に関連するカスタム動作を定義する

 現在位置を表示するという中核の機能が実装できたので、次にマップアプリでよくある、地図表示/衛星画像表示の切り替えや、地図をドラッグして動かしたのちに現在位置表示に戻すような操作を行うボタンやスライドスイッチを配置してみましょう。

 地図画像と衛星画像の切り替えはMapView.MapTypeで行います。表示切替用のスライドスイッチの操作に合わせて値をTMapType.Normal(地図画像)やTMapType.Satellite(衛星画像)に変更すれば表示が切り替わります。

 表示切替用のSwitch1の操作に合わせてMapView1.MapTypeを以下のようなイベントハンドラで処理すれば、マップ表示を通常地図と衛星画像に切り替えられます。

procedure TForm1.Switch1Switch(Sender: TObject);
begin
  // 衛星画像と通常地図の切り替えを行う
  if( Switch1.IsChecked ) then
    MapView1.MapType := TMapType.Satellite
  else
    MapView1.MapType := TMapType.Normal;
end;

 マップ表示を現在位置に切り替える処理は、基本的には最初に実装したOnLocationChangedのときと同じです。ただしボタンクリックのイベントでは現在位置の情報が通知されませんので、OnLocationChangedで測位された情報をあらかじめクラス内のprivate 変数などに保管しておくなどの対処を行っておきます。そして現在位置表示用のボタンイベントでMapView1.Locationに保存済みの現在位置を設定します。

iOSとAndroidの双方に対応する際注意するべきこと

 とはいえ、TMapViewがマルチプラットフォーム環境すべてを完全にカプセル化してくれるわけではありません。iOSの場合はここまでの流れだけで地図表示が行えますが、Androidの場合はプロジェクトのビルドオプションに設定する項目があります。これを忘れるとちゃんと動作してくれません。具体的には以下2点の設定が必要です。

  • 「マップサービス」をTrueに設定する。
  • Google Maps Android APIの管理コンソールで発行したAPI Keyを設定する。

 Delphi向けにGoogle Maps Android APIの管理コンソールでAPI Keyを発行して設定する手順はdocwiki.embarcadero.comで紹介しています。詳細はこちらをご覧ください。

 このように必要な設定を行うことで、iOSとAndroidの両方での地図表示アプリが完成しました。特別なコーディングはしていませんが、iOSとAndroidでタブの位置やボタンのスタイルが、プラットフォーム標準に合わせるように異なっていることに注意してください。

図4 地図表示アプリiOS版とAndroid版

位置情報の詳細を入手する

 アプリでは、地図上に現在位置を表示するだけでなく、詳細な緯度経度や住所情報などを取得して記録したり、関連する情報を検索したりしたいでしょう。

 そこで、今度は位置情報に関する詳細な情報を表示するようにしてみましょう。

TGeocoder

 位置情報から詳細な住所情報などを取得するのに使うのが、TGeocoderです。TGeocoderにはジオコーディング(住所から緯度経度を得る)と逆ジオコーディング(緯度経度から住所を得る)の2つの機能があります。これもTMapViewと同様にプラットフォームごとの実装をコンポーネントがカプセル化しているので、アプリケーション側は単にTGeocoderを利用するだけでiOS、Androidの両方で動くコードが作れます。

 ただしTGeocoderはコンポーネントではなくクラスで提供されているので、TGeocoderを使う場合はイベントハンドラの実装に加えてクラスやプロシジャーの宣言をコードで行います。

TListItemを使って複数の情報をリスト表示する

 取得した情報を表示するには、簡単なコンポーネントであればTLabelなどがあります。しかし、今回は表示項目数が多いとレイアウト調整の手間が掛かりそうなので、TListBoxを使用することにします。

 TListBoxは、複数の項目を持ちます。各表示項目はListBoxItemプロパティです。複数のListBoxItemを作成することで、項目を増やすことができます。

 この操作は、図のように構造ペインのメニューで行うことができます。

図5 ListBoxItemの追加

GeocodeReverseEventを実装する

 次に、TGeocoderを使って現在位置の詳細情報を取得する処理を記述します。TGeocoderの処理は以下の3か所に記述します。

1. フォームのprivate宣言部

 private宣言部に以下のようにFGeocoderオブジェクトとイベントハンドラのプロシジャーの宣言を記述します(太字箇所)。

private
 { private宣言 }
 FGeocoder: TGeocoder;
 Procedure OnGeocodeReverseEvent(const Address: TCivicAddress);

2. LocationSensorのOnLocationChangedイベントへの追加

 OnLocationChangedで取得した位置情報をTGeocoderに引き渡す処理を以下のように記述します(太字箇所)。

procedure TForm1.LocationSensor1LocationChanged(Sender: TObject;
  const OldLocation, NewLocation: TLocationCoord2D);
begin
  // 地図の現在位置情報を書き換える。
  MapView1.Location := TMapCoordinate.Create(
    NewLocation.Latitude, NewLocation.Longitude );
 
  // 現在の緯度経度に対応する住所を取得するための一連の処理。
  if not Assigned(FGeocoder) then
  begin
    if Assigned(TGeocoder.Current) then
      FGeocoder := TGeocoder.Current.Create;
    if Assigned(FGeocoder) then
      FGeocoder.OnGeocodeReverse := OnGeocodeReverseEvent;
  end;
   
  if Assigned(FGeocoder) and not FGeocoder.Geocoding then
    FGeocoder.GeocodeReverse(NewLocation);
end;

3. OnGeocodeReverseEventイベントに対応するイベントハンドラの作成

OnGeocodeReverseEventイベントが発生したら、取得できた住所情報を表示するための処理を記述します。

procedure TForm1.OnGeocodeReverseEvent( const Address: TCivicAddress );
begin
  // 緯度経度から現在位置の住所が取得できた場合は表示を更新する。
  ListBoxItem1.ItemData.Detail := Address.AdminArea;
  ListBoxItem2.ItemData.Detail := Address.CountryCode;
  ListBoxItem3.ItemData.Detail := Address.CountryName;
  ListBoxItem4.ItemData.Detail := Address.FeatureName;
  ListBoxItem5.ItemData.Detail := Address.Locality;
  ListBoxItem6.ItemData.Detail := Address.PostalCode;
  ListBoxItem7.ItemData.Detail := Address.SubAdminArea;
  ListBoxItem8.ItemData.Detail := Address.SubLocality;
  ListBoxItem9.ItemData.Detail := Address.SubThoroughfare;
  ListBoxItem10.ItemData.Detail := Address.Thoroughfare;
end;

 以上で、図のように詳細な情報が表示されるようになります。

図6 位置に関する詳細情報の表示

モーションセンサーを使う

加速度センサーを使って水平器を作る

 最後の機能は少し違った用途かもしれませんが、スマートフォンに搭載された加速度センサーの情報を取得する例です。最近のスマートフォンでは日々の歩数や運動量を自動的に記録してくれたりしますが、それらの機能では加速度センサーの値に基づいてデータの記録を行っています。

 加速度センサーではX軸、Y軸、Z軸の加速度を取得できますが、X軸、Y軸はスマートフォンの横方向、縦方向ですが、Z軸は液晶面に対して手前側、背中側の向きを意味しています。それぞれの軸の値が0ならば、その向きへの動きはないことを意味します。

 ここではこの変化を利用して水平器を作ってみることにします。よくある水平器は色のついた液体を満たした容器の中に気泡があり、傾き具合によって気泡の位置が変わることで水平状態を確認します。今回作る処理ではアプリ画面上に円を表示して、X軸、Y軸の値に応じて表示位置を変えることにします。

TPlotGrid と TCircle で水平器の外観を作る

 UIに関しては、あらかじめ用意されたコンポーネントを使って、簡単に構築してしまうことにします。

 役立ちそうなのがTPlotGridとTCircleです。TPlotGridは方眼紙状のグリッドを表示するコンポーネントです。TPlotGridを配置したら、Frequncyプロパティを適切な値(この場合は20に設定しました)を指定し、方眼のマス目を調整します。そして、この上に円を表示するTCircleを配置すれば、水平器の基本デザインの完成です。簡単ですね。

図7 水平器のUI設計

加速度を取得する

 加速度はTMotionSensorで取得できます。ただしTMotionSensorでは、値が変化したときにイベント通知を受け取るという動作ができません。そこで、TTimerを使ってで定期的に値の読み取りを行うようにします。

 TTimerのOnTimerイベントハンドラに、以下のコードを記述します。このコードは、TTimerのIntervalプロパティに指定したミリ秒ごとに呼び出されます。今回は、この値を100(=0.1秒)にしてみました。

procedure TForm1.Timer1Timer(Sender: TObject);
var
  // 加速度。
  syntheticAccel: double;

  // X,Y,Z軸の加速度。
  AccelX: double;
  AccelY: double;
  AccelZ: double;
begin
  // x,y,z軸の加速度を取得して値を100倍にする。
  // (値の変化を誇張するため)
  AccelX := MotionSensor1.Sensor.AccelerationX * 100;
  AccelY := MotionSensor1.Sensor.AccelerationY * 100;
  AccelZ := MotionSensor1.Sensor.AccelerationZ * 100;

  // 3軸の合成加速度を算出する。
  // これは加速度ベクトルの大きさ(スカラー成分)だけを取り出す処理。
  // 3軸の加速度が変化しても合成加速度に変化がなければ、
  // 物体の運動は変化していないと判断できる。
  syntheticAccel := 
    sqrt( power(AccelX,2) + power(AccelY,2) + power(AccelZ,2) );

  // 取得した値をラベルに出力する。
  Label1.Text := Format( 'X: %3.2f', [ AccelX ] );
  Label2.Text := Format( 'Y: %3.2f', [ AccelY ] );
  Label3.Text := Format( 'Z: %3.2f', [ AccelZ ] );
  Label4.Text := Format( 'accel: %3.2f', [ syntheticAccel ] );

  // Circle を x, y 軸の加速度の値に合わせて Grid に重ねて表示する。
  Circle1.Position.X :=
    ((PlotGrid1.Width  - Circle1.Width )/2.0) - (AccelX*10.0);
  Circle1.Position.Y :=
    ((PlotGrid1.Height - Circle1.Height)/2.0) + (AccelY*10.0);
end;

 取得した加速度の値で円の位置を動かす処理は、TCircleのPosition.XとPosition.Yを書き換えるだけです。X軸とY軸の加速度が0のときにTPlotGridの中心に円の位置が定まるような計算式を作り、その計算式でPosition.XとPosition.Yを求めるようにしています。

アプリの完成

 アプリには、X、Y、Z軸の加速度、3軸合成値(加速度のスカラー値)などの数値情報なども表示されるようにしてみました。

図8 水平器の表示

レイアウトについて

 今回は次のような方針でユーザーインターフェイスを作りこんでいます。

  • 各々の機能はタブで切り替えるようにする
  • 各機能はスマートフォンの画面全体を使い、スクロールは行わない

 タブ切り替えは提供する機能の数が少ないうちはよいのですが、増えてくると使い勝手が悪くなります。この場合はマルチビューコンポーネントを使うなどの別の方法を検討してください。

タブ機能を簡単に実装するTTabControl

 タブで画面切り替えを行うには、TTabControlコンポーネントを使用します。今回は、これをデザインフォームにあらかじめ配置しました。そしてここが重要なのですが、スマートフォンの画面サイズによらずTTabControlでデザインフォーム全体を使用するために、AlignプロパティをClient に設定します。

 Alignプロパティは「コンポーネントがフォームにどのように配置されるか」を制御します。Align = Clientと設定したコンポーネントはフォームの大きさに合わせて自動的にサイズ変更されます(より正確にいうと、自分の親オブジェクトの大きさに合うようにサイズ変更されます)。

図9 TTabControlを配置

 設計画面でタブをクリックするか、TTabControlのActiveTabプロパティを変更すると、タブページを切り替えることができます。タブページを新規に追加するには、構造ビューでTabControlを右クリックして項目エディタを実行するか、TTabItemの追加を選びます。

マルチデバイスできれいに表示させるための配慮

 例えば、最初の地図表示ページでは、以下のような画面構成になっています。

図10 地図表示ページの画面構成

 それぞれのAlignプロパティを適切に設定することで、画面サイズが変わっても相対的に正しくレイアウトされるようになります。

 このほかにも、項目間の余白を設定するMargin、子項目との余白を設定するPaddingなどのプロパティによって配置間隔を調整したり、Anchorプロパティによって、上下左右のいずれか(または複数)に位置を寄せる設定が可能です。

それでもデバイスごとにカスタマイズしたいときのFireUI

 とはいえ、UI設計がいつもこれらのプロパティで完全に制御できるとは限りません。そのような場合に役立つのがFireUIマルチデバイスデザイナです。

 FireUIマルチデバイスデザイナは、マスターUIを作成したら、そこからデバイス固有のUIを定義できます。例えばNexus 5のUIだけカスタマイズ、というようにです。

図11 FireUIマルチデバイスデザイナ

 マスターUIの変更は、それを継承したすべてのカスタムUIに反映されます。一方、カスタムUIの変更は、そのデバイスのみに適用されます。

 これにより、共通部分はそのまま単一のUI設計で残しながらも、固有の部分だけカスタム化できるのです。

おわりに

 今回紹介した機能は、Delphiの最新バージョンDelphi 10.1 Berlinで利用できます。C++言語で開発したいという方は、C++Builderで同様の開発が可能です。両方の言語をミックスして使用する方には、スイート製品のRAD Studioがおすすめです。

 本記事で紹介したアプリは、エンバカデロのWebセミナー「Delphi Boot Camp」でも取り上げています。Delphi Boot Campは、9月12日~15日まで毎日16時に開催しており、終了したセッションは、オンデマンドでご覧いただけます。

 「翔泳社 SEshop」では、このWebセミナーをご覧いただいた方を対象にお得なキャンペーンを実施しています。

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • EDN編集部(イーディーエヌ編集部)

    エンバカデロ・デベロッパーネットワーク(EDN)は、ソフトウェア開発者とデータベース技術者のための技術情報サイトです。Delphi、C++Builderをはじめとする開発ツールやER/Studioなどのデータベースツールに関連する技術記事、ビデオなどを提供しています。EDN編集部は、EDN記事と連携...

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5