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 );
}