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を追加している。