TImageViewerなどでZoomジェスチャーの処理をする

TImageViewerなどでZoomジェスチャーの処理について探せる範囲で情報が無いのでメモ。

処理は幾つかの状態保存変数とOnGestureイベントの記述で済む。
まず、TImageViewerのTouchプロパティのInteractiveGesuturesを開いてZoomをTrueにする。
OnGestureコードは、以下のようにすれば良いと思うだろう。

// 状態保存変数 Formのprivate宣言など適当なところで定義すること
double  FStartDistance;

// OnGesuture
void __fastcall TForm1::ImageViewer1Gesture(TObject *Sender, const TGestureEventInfo &EventInfo,
          bool &Handled)
{
  if( System::Word(EventInfo.GestureID) == igiZoom )
  {
    if( EventInfo.Flags.Contains( TInteractiveGestureFlag::gfBegin ) )
    {
      FStartDistance       = EventInfo.Distance;
    }
    else if( EventInfo.Flags.Contains( TInteractiveGestureFlag::gfEnd ) )
    {
      ImageViewer1->BitmapScale = ImageViewer1->BitmapScale * EventInfo.Distance / FStartDistance;
    }
  }

テストをすると、思ったようにサイズが変更されない。動作状況をデバッガで見ると、EventInfo.Distanceが正しくセットされていない場合があることに気がついた。特に、gfEndでは正しくない場合が多い。テストをした範囲で、異常なケースのDistanceは、値が0~2となるようだ。
また、通常指でピンチした位置を中心にズームさせたいのだが、上のコードではそうならない。

対策を入れてまともに機能するコードを書くと以下のようになる。

// 状態保存変数 Formのprivate宣言など適当なところで定義すること
double  FStartDistance;
double  FLastDistance;
TPointF FOrgLocation;

// OnGesuture

void __fastcall TForm1::ImageViewer1Gesture(TObject *Sender, const TGestureEventInfo &EventInfo,
          bool &Handled)
{
  constexpr int DIST_THRESOLD = 10;
  if( System::Word(EventInfo.GestureID) == igiZoom )
  {
    if( EventInfo.Flags.Contains( TInteractiveGestureFlag::gfBegin ) )
    {
      FStartDistance       = ( EventInfo.Distance > DIST_THRESOLD )? EventInfo.Distance : 0;
      FLastDistance        = FStartDistance;
      FOrgLocation         = ImageViewer1->AbsoluteToLocal( EventInfo.Location );
    }
    else if( EventInfo.Flags.Contains( TInteractiveGestureFlag::gfEnd ) )
    {
      if( EventInfo.Distance > DIST_THRESOLD )
        FLastDistance = EventInfo.Distance;
      
      double d = ImageViewer1->BitmapScale * FLastDistance / FStartDistance;
      if( d == 0.0  ) return;
      if( d <  0.01 ) d = 0.01;
      if( d >  1.0  ) d = 1.0;
      double dx = d / ImageViewer1->BitmapScale;
      TPointF po = ImageViewer1->ViewportPosition;
      po.X = dx*( po.X + FOrgLocation.X ) - FOrgLocation.X;
      po.Y = dx*( po.Y + FOrgLocation.Y ) - FOrgLocation.Y;
      ImageViewer1->BitmapScale      = d;
      ImageViewer1->ViewportPosition = po;
    }
    else
    {
      if( EventInfo.Distance <= DIST_THRESOLD )
        return;

      if( FStartDistance == 0 )
        FStartDistance = EventInfo.Distance;
      else
        FLastDistance = EventInfo.Distance;
    }
  }
}

閾値(DIST_THRESOLD)を10にしているのは適当だ。また処理ではスケールが0.01~1.0の値となるようにしている。
ピンチ中も画像のズームを追従させたいのであれば上記else節にズーム変更のコードを書けば良いが、一手間かける必要がある。
以下にコードを示す。

// 状態保存変数 Formのprivate宣言など適当なところで定義すること
TPointF FOrgViewportPosition;
double  FOrgScale;
double  FStartDistance;
double  FLastDistance;
TPointF FOrgLocation;

void TForm1::zoom_update()
{
  double d = FOrgScale * FLastDistance / FStartDistance;
  if( d == 0.0  ) return;
  if( d <  0.01 ) d = 0.01;
  if( d >  1.0  ) d = 1.0;
  double dx = d / FOrgScale;
  TPointF po;
  po.X = dx*( FOrgViewportPosition.X + FOrgLocation.X ) - FOrgLocation.X;
  po.Y = dx*( FOrgViewportPosition.Y + FOrgLocation.Y ) - FOrgLocation.Y;
  ImageViewer1->BeginUpdate();
    ImageViewer1->BitmapScale      = d;
    ImageViewer1->ViewportPosition = po;
  ImageViewer1->EndUpdate();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ImageViewer1Gesture(TObject *Sender, const TGestureEventInfo &EventInfo,
          bool &Handled)
{
  constexpr int DIST_THRESOLD = 10;
  if( System::Word(EventInfo.GestureID) == igiZoom )
  {
    if( EventInfo.Flags.Contains( TInteractiveGestureFlag::gfBegin ) )
    {
      FOrgViewportPosition = ImageViewer1->ViewportPosition;
      FOrgScale            = ImageViewer1->BitmapScale;
      FStartDistance       = ( EventInfo.Distance > DIST_THRESOLD )? EventInfo.Distance : 0;
      FLastDistance        = FStartDistance;
      FOrgLocation         = ImageViewer1->AbsoluteToLocal( EventInfo.Location );
    }
    else if( EventInfo.Flags.Contains( TInteractiveGestureFlag::gfEnd ) )
    {
      if( EventInfo.Distance > DIST_THRESOLD )
        FLastDistance = EventInfo.Distance;
      zoom_update();
    }
    else
    {
      if( EventInfo.Distance <= DIST_THRESOLD )
        return;

      if( FStartDistance == 0 )
      {
        FStartDistance = EventInfo.Distance;
        return;
      }

      FLastDistance = EventInfo.Distance;
      zoom_update();
    }
  }
}

位置変更のコードをzoom_update()関数にまとめ、スケールと位置変更中は再描画しないようにBeginUpdateとEndUpdateを追加している。

Firemonkeyジェスチャーイベント処理

この情報についてはC++ Builder 10.4.1(Sydney)で確認した情報なので、将来変わる可能性がある。
と言うか、昔と変わった部分についてEmbarcaderoがドキュメントの更新をしていないための備忘だ。

まず、ジェスチャーのIDの定義だが、現時点でSystem.UITypes.hppに移動している。Fmx.UITypes.hppではない。
昔igZoomなどとしているのであれば、名前がigiZoomに変わっているので注意が必要だ。

次に、上記IDと、TGestureEventIDでのGestureIDでは型が異なる。IDはSystem::Word型、GestureIDはTGestureID型で、比較用の関数は準備されていないので以下のようなコードを書くとエラーが表示される。なぜかSystem:Wordへの型変換関数はあるようで、代入やswitch文は通る。

  if( EventInfo.GestureID == igiZoom ) ;// オーバーロード演算子==エラー

  if( System::Word(EventInfo.GestureID) == igiZoom ) ; // OK

  System::Word w = EventInfo.GestureID; // OK

  switch(  EventInfo.GestureID ) 
  {
    case igiZoom: break; // OK
  }

比較関数は用意しておいて欲しいと思う。

ZoomなどのインタラクティブジェスチャーではgfInertiaで途中経過が得られるように見えるが、gfInertiaは発生しない。
途中経過を得るには以下のようなコードを書く。

void __fastcall TForm1::ImageViewer1Gesture(TObject *Sender, const TGestureEventInfo &EventInfo,
          bool &Handled)
{
  if( System::Word(EventInfo.GestureID) == igiZoom )
  {
    if( (!EventInfo.Flags.Contains( TInteractiveGestureFlag::gfBegin ) )
      &&(!EventInfo.Flags.Contains( TInteractiveGestureFlag::gfEnd   ) ) )
    { 
      // 途中経過の処理コード
    }
  }

C++ Builderであればセットが空のチェックでもいける

void __fastcall TForm1::ImageViewer1Gesture(TObject *Sender, const TGestureEventInfo &EventInfo,
          bool &Handled)
{
  if( System::Word(EventInfo.GestureID) == igiZoom )
  {
    if( EventInfo.Flags.Empty() )
    { 
      // 途中経過の処理コード
    }
  }

Androidでカメラを利用するには

Firemonkeyを用いてAndoroidアプリを開発し、アプリにカメラ機能を追加するには2つの方法がある。

  • TCameraComponentを使って、アプリ内で撮影できるようにする。
  • TTakePhotoFromCameraアクションかIFMXCameraServiceを用いて、他のカメラアプリで撮影する。

A.TCameraComponentを用いる

TCameraComponentを用いた方法は、TCameraComponentのヘルプや、そのサンプルがあるのでそちらを参考にして欲しい。
ただ、なぜだか分からないが、手元にあるAndroidタブレットでは、カメラの解像度を最高にできないなど制限がある。
キャプチャ頻度も良くないのでプレビューではカクカク表示される。性能の高いAndroidタブレットであればそうはならないかもしれない。
得られる画像は、どうもビデオストリーミングのキャプチャー画像となるようでぶれているケースが目立つ。
使用に当たっては目的に合致するかよく考えた方が良いだろう。
TCameraComponentを利用するには、プロジェクトオプションの「使用する権限」で「カメラの利用」にチェックをつける必要がある。アプリのオプションで設定してもデバッグでは権限の確認画面が表示されないので、デバッグインストールの後で、Androidタブレットの設定を開きアプリの権限で設定してやる。

B.TTakePhotoFromCameraアクションを用いる

TTakePhotoFromCameraアクションを用いる場合、通常コントロール(TButtonなど)のActionに、TTakePhotoFromCameraアクションを関連付ける。
こちらの方法では、プレビューもスムーズだし、撮影した画像も気をつけていればぶれない。
行う作業等を以下に示す。

  1. ActionListコンポーネントを追加する
  2. ActionListコンポーネントをダブルクリックするなどしてアクションリストの編集ダイアログを表示する
  3. 標準アクションの作成を選んで、メディアライブラリカテゴリにあるTTakePhotoFromCameraActionを追加する
  4. 追加したアクションのプロパティ(MaxWidth,MaxHeight)に大きめの値を入れておく(5000位)
  5. 追加したアクションのOnDidFinishTakingイベントを作成して引数のTBitmapを処理するルーチンを作成する。
  6. 追加したアクションをコンポーネントのActionに関連付けするなどする
  7. プロジェクトオプションの「資格リスト」で「セキュアファイルの共有」にチェックをつける

7.を行わないで実行すると、カメラアプリ呼び出し時にエラーメッセージが発生する。この情報はTTakePhotoFromCameraActionのヘルプを見ても説明がないので、困る方もいるだろう。なお、ヘルプのトピック→「資格リスト」の該当項目には記述がある。
ここまでして画像を得ることができるが、実はこちらの方法でもカメラの最大解像度で画像を得ることができない場合がある。また、TTakePhotoFromCameraActionのNeedSaveToAlbamプロパティにチェックをつけて、共有に画像を別に保存したものと比較するとサイズがかなり小さくなっており、高圧縮の画像であることが分かる。
実機でのテストでは、4160x3120ピクセルの画像を得られるAndroidタブレットで、2080x1560ピクセルという半端なサイズの画像が得られる。別のAndroidタブレットでは、4032x1960ピクセルの画像が4032x1960ピクセルになる。

いろいろ挙動を確認したところ、Androidでカメラアプリとの画像のやりとりは以下のような手続きになっているようだ。

  1. カメラアプリの呼び出し(インスタンスと最大画像サイズの受け渡し)
  2. シャッターボタンを押して撮影(この瞬間、テンポラリー画像ファイルのデータが作成される)
  3. 画面にはい・いいえボタンが表示される
  4. はいを選ぶとカメラアプリ終了(NeedSaveToAlbamがTrueであれば、このタイミングで保存ファイルが作成される)
  5. テンポラリー画像ファイルが保存される
  6. 呼び出し元のアプリにテンポラリー画像ファイルのパスを通知
  7. 呼び出し元のアプリにテンポラリー画像ファイルのイメージデータを通知
  8. テンポラリー画像ファイルを消去(されない行儀が悪いAndroidタブレットもあり)

見て分かるように、渡される画像は一回ファイルとして保存がされている。
この画像ファイルは、NeedSaveToAlbamフラグで保存される画像ファイルとは別で、Exif情報がなく、また高圧縮である。画像サイズについてはカメラアプリの実装次第で最大の画像サイズでない場合もある。
6.で渡される画像ファイル名はテンポラリー画像ファイルのファイル名で、NeedSaveToAlbamフラグで保存される画像ファイルの名前ではない。

C.IFMXCameraServiceとTMessageReceivedImagePathメッセージを用いる

B.のアクションイベントのかわりにIFMXCameraServiceとTMessageReceivedImagePathメッセージを用いて、撮影保存された画像を得る方法を示す。
このコードはAndroidのみで動くことに注意。
幾つか仕込む必要があるのと、RTLメッセージフックの登録解除を行う必要があるので、慎重に実装して欲しい。

まず、メッセージフックの登録解除のコードを示す。登録はフォーム生成時、解除はフォームクローズのタイミングが良いだろう。
コード中のReceivedImagePathMessageがメッセージを受け取る関数だ。また、FIdは登録時の値を保持して、解除の時に与える必要がある。

//---------------------------------------------------------------------------
#include <Fmx.Platform.Android.hpp>
int FId;
void __fastcall TForm1::FormCreate(TObject *Sender)
{
  // TMessageReceivedImagePathメッセージを受け取る関数の登録
  TMetaClass*            msgCls         = __classid(Fmx::Platform::Android::TMessageReceivedImagePath);
  TMessageListenerMethod listenerMethod = &(this->ReceivedImagePathMessage);
  FId = TMessageManager::DefaultManager->SubscribeToMessage( msgCls, listenerMethod );
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
  // メッセージ登録解除
  if( FId != 0 )
  {
    TMetaClass* msgCls = __classid(Fmx::Platform::Android::TMessageReceivedImagePath);
    TMessageManager::DefaultManager->Unsubscribe( msgCls, FId );
  }
}
//---------------------------------------------------------------------------

ボタンクリックイベントでのカメラアプリの呼び出し例を以下に示す。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  _di_IFMXCameraService service;
  TParamsPhotoQuery params;
  if(  TPlatformServices::Current->SupportsPlatformService(__uuidof(IFMXCameraService) )
   &&( service = TPlatformServices::Current->GetPlatformService(__uuidof(IFMXCameraService))) )
  {
    params.Editable           = false;
    params.NeedSaveToAlbum    = true;
    params.RequiredResolution = TSize( 8192, 8192 );
    params.OnDidFinishTaking  = nullptr;
    service->TakePhoto( SnapButton, params );
  }
  else
  {
    ShowMessage( "This device does not support the camera service" );
  }
}

RequiredResolutionには適当な大きい数字を入れる。また、NeedSaveToAlbumはtrueにする。

テンポラリー画像のファイル名を得て、そこから保存ファイル名を検索して、画像を読み込むコードを以下に示す。保存ファイルの生成はテンポラリー画像ファイルと最大でも2秒程度の誤差しかないと仮定している。

constexpr double dEps = 1.0/( 24.0 * 60.0 * 30.0 ); // 2秒
void __fastcall TForm1::ReceivedImagePathMessage( System::TObject* const Sender, System::Messaging::TMessageBase* const M )
{
  TMessage__1<UnicodeString>* Message = dynamic_cast<TMessage__1<UnicodeString>*>(M);
  if( Message == nullptr )
    return;

  // 保存画像ファイル名をファイルのタイムスタンプで検索
  UnicodeString uSrc  = Message->Value;
  UnicodeString uPath = ExtractFilePath( uSrc );

  TDateTime       srcTime = TFile::GetLastWriteTime( uSrc );
  TStringDynArray aFiles  = TDirectory::GetFiles( uPath, u"*.*" );
  for( int i = 0; i < aFiles.Length; ++i )
  {
    if( aFiles[i] == uSrc ) continue;
    TDateTime dTime = TFile::GetLastWriteTime( aFiles[i] );
    if( fabs( dTime.Val - srcTime.Val ) < dEps )
    {
      uSrc    = aFiles[i];
      srcTime = dTime;
      break;
    }
  }
  TBitmap* Image = new TBitmap;
  Image->LoadFromFile( uSrc ); 
}

ボタンクリックコードの替わりにB.のアクションイベントを用いても可能であるが、メッセージフックは自前で書かなければならないのでこちらの方が見通しが良いと思う。

Firemonkeyでスクロールボックスのスクロール位置を指定する

TScrollBox,TVertScrollBox,THorzScrollBoxといったTCustomScrollBoxの派生クラスをレイアウトベースとして使う場合、スクロールの位置を知りたい場合がある。
名称にScrollが付いていないのでわかりにくい(ヘルプもわかりにくい)がViewportPositionプロパティで得ることが可能だ。

  TPointF pf = ScrollBox1->ViewportPosition;
  float fHpos = pf.X; // 水平スクロール位置
  float fVpos = pf.Y; // 垂直スクロール位置

逆にスクロール位置を指定する場合もViewportPositionで行うのが簡単だ。
水平スクロール位置をHScrollPos、垂直スクロール位置をVScrollPosとすれば以下のような感じになる。

  ScrollBox1->ViewportPosition = TPointF( HScrollPos, VScrollPos );

ScrollByメソッドは相対位置指定で、かつ逆方向なので注意が必要、絶対位置スクロールを行う場合のViewportPositionとScrollByメソッドで等価なコードを示す。

  // スクロール位置を50,50から120,120へ変更する場合のコード
  // ViewportPositionで行う場合
  ScrollBox1->ViewportPosition = TPointF( 120.0, 120.0 );

  // ScrollByで行う場合
  TPointF pf = ScrollBox1->ViewportPosition;
  ScrollBox1->ScrollBy( pf.X - 120.0, pf.Y - 120.0 );

相対位置指定の上、移動量の指定が逆方向なので現在位置からマイナスしなければならない。

スクロールの最大値は得ることができないようだが、スクロールボックス内のコンテンツの範囲を得ることで代用可能だと思う。

  TRectF rf = ScrollBox1->ContentBounds;
  float fHmax = rf.right; // 水平最大
  float fVmax = rf.bottom; // 垂直最大

Firemonkeyで次のタブストップオブジェクトを得る

VCLフレームワークであれば、以下のコードでタブで移動する先のコントロールを得ることができる。

  TWinControl* pNext = FindNextControl( Edit1, true, true, false );

しかしFiremonkeyフレームワークにはFindNextControlに相当する機能はない。
いろいろ探してみたところFindNextTabStopを用いて、以下のようなコードを記述する。

// 検索関数
TControl* find_next_control( TControl* pCurrent )
{
  for(;;)
  {
    _di_IControl iControl;
    pCurrent->GetInterface( iControl );

    TControl* pParent = pCurrent->ParentControl;
    if( pParent == nullptr )
    {
      TForm* pForm = dynamic_cast<TForm*>(pCurrent->Parent);
      if( pForm == nullptr )
        return nullptr;

      _di_IControl iFNext = 
                     pForm->GetTabList()->FindNextTabStop( iControl, true, true );
      if( iFNext == nullptr )
        return nullptr;
      return dynamic_cast<TControl*>(iFNext->GetObject());
    }
    else
    {
      _di_IControl iCNext = 
                     pParent->GetTabList()->FindNextTabStop( iControl, true, true );
      if( iCNext != nullptr )
        return dynamic_cast<TControl*>(iCNext->GetObject());

      pCurrent = pParent;
    }
  }
}
// 利用側
void TForm1::OnEdit1Click( TObject* Sender )
{
  TControl* pNext = find_next_control( Edit1 );
}

自分のIControlインターフェースを得た上で、親コントロールタブリストを検索し、無ければさらに上位のコントロールタブリストで検索する。TFormはTControlの派生クラスではない為、場合分けをする。
VCLフレームワークと異なり、フォームのGetTabListだけでは正しく得ることができないことに注意だ。
コンパイラーをBorlandコンパイラー(bcc32.exe)を用いる場合はnullptrをNULLに置き換えすること。
無条件ループを'for(;;)'にするか'while(true)'にするかは好みだと思う。

なお、FindNextTabStopはアンドキュメントなので、今後のバージョンで利用可能なのかは不明だ。
できればVCLと同じように扱えると便利なのだが。

Firemonkeyでキーボードに重ならないように編集中のコントロールの位置を変更する(ずらす)方法

FiremonkeyでAndroidアプリケーションのテキストの入力を行うときに、仮想キーボードが出現して入力するコントロールを覆い隠す場合は、フォーム全体を上方向にずらしてコントロールを表示させたい。
Firemonkey以外で開発しているのであれば詳細は割愛するが、マニフェストに「android:windowSoftInputMode="adjustPan"」などとすれば良い。
残念なことにFiremonkeyではこの手法でずらすことはできない。なぜならFiremonkeyはコントロールを自前で描画しているためだ。

ではどうすれば良いのか? 参考にしたのは以下の記事だ。
[小ネタ]色々使えるVertScrollBox - Qiita

上記記事のようにTVertScrollBoxを使うのも一手であるが、別の解を以下に示す。

1.仮想キーボードの高さを決め打ちで行う方法

上にずらすコードは以下の通り。

void rollup( const TForm* pForm, TControl* pBase, TControl* pCtrl, const float fSlide )
{
  TPointF absCtrl = pCtrl->LocalToAbsolute( TPointF( 0.0, 0.0 ) );
  TVector lV      = TVector::Create( 0.0, pCtrl->Height, 0.0 );
  TVector v       = pCtrl->LocalToAbsoluteVector( lV );
  int     nBottom = absCtrl.Y + v.Y;
  int     nSlide  = pForm->Height * fSlide;

  if( nBottom <= nSlide )
  {
    unroll( pBase );
    return;
  }

  pBase->Align       = TAlignLayout::None;
  pBase->Position->Y = nSlide - nBottom;
}

pBaseが、上にずらすコントロール(TPanelなど)で、pCtrlが編集対象となるコントロール(TEditなど)だ。
下記のコードでは入力状態になるpCtrlがキーボードに隠れる場合、下端が"画面全体の高さ*fSlide"の位置になるようにする。
また、ずらすベースコントロールのAlignフィールドはClientとなっていると仮定している。
位置と高さの計算は、LocalToAbsoluteおよびLocalToAbsoluteVectorを使ってForm上の絶対位置を得てから計算しないとだめだ。
画面への表示はFiremonkeyがpCtrlのPositionやHeightを基にスケーリングや入れ子のパディングなどを計算するためそのままは使えない。

元に戻すコードは以下の通り。

void unroll( TControl* pBase )
{
  pBase->Align = TAlignLayout::Client;
}

上記関数を使ったコードの例を示す。
コード中のオブジェクトの名称については以下の図の通りとする。

f:id:konishih:20201111101124p:plain:h300
設定画像
void __fastcall TForm1::Edit1Enter(TObject *Sender)
{
  rollup( Form1, Panel1, Edit1, 0.6 );
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Edit1Exit(TObject *Sender)
{
  unroll( Panel1 );
}
//---------------------------------------------------------------------------

この方法の良いところは、スクロールバーの設定をしなくても良いのでTVertScrollBoxであらかじめコントロール分のスクロール量を確保しなくても良いこと、またベースがTPanelで無くとも使えるところだ。
悪いところは、OnExitイベントが正しく発生しなかった場合に位置を元に戻す方法がないことと、仮想キーボードが表示されなくても上に移動することだ。

2.仮想キーボードの高さを得て行う方法

上にずらすコードは以下の通り。unroll関数は1.と変わらないので割愛する。

const int NO_KEYBOARD_PARAM = -1;
void rollup( TControl* pBase, TControl* pControl, const int nKbdTop = NO_KEYBOARD_PARAM )
{
  static int       nTop  = NO_KEYBOARD_PARAM;
  static TControl* pCtrl = nullptr;
  if( pControl == nullptr ) nTop  = nKbdTop;
  else                      pCtrl = pControl;

  if( ( nTop < 0 )||( pCtrl == nullptr ) )
  {
    unroll( pBase );
    return;
  }

  TPointF absCtrl = pCtrl->LocalToAbsolute( TPointF( 0.0, 0.0 ) );
  TVector lV      = TVector::Create( 0.0, pCtrl->Height, 0.0 );
  TVector v       = pCtrl->LocalToAbsoluteVector( lV );
  int     nBottom = absCtrl.Y + v.Y - pBase->Position->Y;

  if( nBottom <= nTop ) 
  {
    unroll( pBase );
    return;
  }

  pBase->Align       = TAlignLayout::None;
  pBase->Position->Y = nTop - nBottom;
}

仮想キーボードの上端位置はTFormにあるOnVirtualKeyboardShownイベントで得る。
関数中のstaticな変数は状態保持変数で、OnEnterイベントとOnVirtualKeyboardShownイベントの二つにまたがっての処理のために記述している。
そうする理由を以下に挙げる。

  1. OnVirtualKeyboardShownイベントは仮想キーボードを利用する編集コントール間の移動では発生しないので、OnEnterイベントでもrollupできるようにする必要がある(下図)。
  2. 下図のようにイベントはOnEnterが先に発生して、その後OnVirtualKeyboardShownイベントが発生するので、OnEnterイベントの初回発生時では適切な仮想キーボードの上端位置が得られない(しかし後でOnVirtualKeyboardShownイベントが発生するのでコントロールを保存する必要がある)。
  3. OnVirtualKeyboardShownイベント内では、フォーカスを得ようとしている編集コントロールポインターを得る手段がない。
f:id:konishih:20201113160838p:plain:h300
イベントフロー

引数pControlがnullptrである場合、nTopに値を代入するようにすることでリセットも可能にしている。
nBottomの計算でpBase->Position->Yを引く処理は、unroll関数の呼び出し元をOnVirtualKeyboardHiddenイベントにした場合に必要となる。OnExitイベントで毎回unrollを呼び出す場合は不要だ。


上記関数を使ったコードの例を示す。

void __fastcall TForm1::Edit1Enter(TObject *Sender)
{
  rollup( Panel1, Edit1, NO_KEYBOARD_PARAM );
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormVirtualKeyboardShown(TObject *Sender,
                                    bool KeyboardVisible, const TRect &Bounds )
{
  rollup( Panel1, nullptr, KeyboardVisible ? Bounds.Top : NO_KEYBOARD_PARAM );
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormVirtualKeyboardHidden(TObject *Sender,
                                    bool KeyboardVisible, const TRect &Bounds )
{
  unroll( Panel1 );
}
//---------------------------------------------------------------------------

Bounds.Topは仮想キーボードの上端位置を示す。KeyboardVisibleがfalseの場合はリセットするようにしている。
位置を戻すコードはOnVirtualKeyboardHiddenイベントで記述しているが、元ネタによれば、IMEの種類によっては発生しない場合もあるらしいので、素直にOnExitに記述する方が良いかもしれない。

1.と比較して良い点は、仮想キーボードの実際の高さで指定できること、またOnVirtualKeyboardShownイベントが発生して、かつKeyboardVisibleがtrueにならない限り、rollupで上へ移動しないことだ。このため、マルチプラットフォームでも同じコードを使える上、試してはいないがAndroidなどで物理キーボードを利用する場合も利用できるだろう。
悪い点は複雑な上に状態保持変数を利用しているので可読性がおちる点だ。

OnEnterイベントは必要となるコントロール毎にイベント関数に紐付けする必要があるが、複数の場合は下の例のようにして同じイベント関数を呼び出しすれば、記述は一箇所で済み見通しが良くなる。

void __fastcall TForm1::AnyEditEnter(TObject *Sender)
{
  rollup( Panel1, dynamic_cast<TControl*>(Sender), NO_KEYBOARD_PARAM );
}

FiremonkeyでAndroidアプリに外部ファイルを添付する

FiremonkeyでAndroidアプリを開発するときに、アプリで使用するファイルを配置する方法についてメモ。

  1. IDEのメニューで「プロジェクト」→「配置」をクリックして配置画面を表示させる(<アプリ名>の配置ではない)。
  2. 上のバーの用紙に+がついたアイコンがファイルの追加なのでそれをクリックして、追加したいファイルを指定。
  3. リモートパスを".\"から"assets\internal"に変更する。

上記の方法で仮にaaa.datと言うファイルを配置すると、Andoroid上では、/data/user/0/<App ID>/files/aaa.datに保存される。
プログラム上でアクセスする場合、以下のような感じだ。

String uAaaDat = System::Ioutils::TPath::Combine( System::Ioutils::TPath::GetDocumentsPath(), u"aaa.dat" );

filesのサブフォルダーを作成してそこに配置したい場合は、上記の"assets\internal\フォルダー名"と記述する。

この辺のドキュメントは、ヘルプであればメインヘルプ、オンライドキュメントであればトピックの、Andoroidアプリケーションの作成ページの下の方にある「ファイルの読み込みと配置」に記述されている。