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