Delphi XE4でのMultiTouch + Gesturesのサポート



どういうわけか、ユーザーが自分の指で突くことができるアクティブモニターの形でのこれらの新しいトレンドはすべて、私に気付かれずに通過しました。 3か月前にボスが2つの部分(画面、キーボード)に分割できるラップトップを購入しなかった場合、それらについては知りません。MSによって推進されるSurfaceではなく、 ASUS 、はるかに少ないお金で(比較的)。
そして、このデバイスは理由のために購入されました-そこからタスクが成長し、そこから彼らは待ちませんでした。

デジュレ:私たちは月に膨大な数のセミナーを開催し、それが彼らに課されていたので、講師たちはこの悪名高いタコとソフトウェアの非互換性を実証し始めました。

デファクト:プランユーザーの怒りの手紙がサポートメールに注がれ始めました-「テープを2回録音しましたが、タップしませんでした。
当局は、これらすべてを「破れた」ラップトップで綿密に監視し、TKを準備しました。

そして、その日が来ました。 LGから23インチ最大 10本の指でタッチ入力をサポート)の3番目のモニターがデスクトップに設置され、タスクが設定されました-これは3日以内に機能するはずです!

そして、私はXE4で働いています-トラブル。

0.問題分析


幸いなことに、私は多くの有能な仲間(Embarcadero MVPを含む)に精通しており、どちらの側でタッチサポートにアプローチするかについて相談できますが、...マルチタッチのサポートに関する技術記事へのリンクを読んで、XE4何も輝いていない。 私が利用できるVCLオプションは非常に限られています。

Embarcaderoカンファレンスを少し読んだ後、いくつかの制限はあるものの、マルチタッチがXE7でのみ利用可能になったことを知りました(ただし)。

問題を解決する最も簡単な方法がXE7のアップデートのようだと言った場合(さらに、apa後に互換性コードをチェックするのにかかる時間)、上司が何を評価するかわかりません。

したがって、XE4で利用できるものを確認します。
プラス:
-彼女はジェスチャー(ジェスチャー)を知っています。
短所:
-彼女はタッチについては知りません(彼女は知っていますが、外部ハンドラーは提供しません)。
-2つの入力ポイント(2本以上の指)を持つジェスチャについては知りません。

次に、利用できないものを見てみましょう。

  1. クラスの厳密なプライベートタイプセクションでもTPlatformGestureEngine内で非表示になっているだけで、IStylusAsyncPluginへのヒープへのIRealTimeStylus3インターフェイスのサポートを導入してTRealTimeStylusクラスを拡張することはできません。
  2. このメッセージはTWinControl.WndProc内で処理されますが、本格的なWM_TOUCHメッセージハンドラは提供されていません。


WM_TOUCH: with FTouchManager do if (GestureEngine <> nil) and (efTouchEvents in GestureEngine.Flags) then GestureEngine.Notification(Message); 


コードからわかるように、コントロールはジェスチャ認識エンジンに直接移動します。
どうやら-ジェスチャが認識しない順序でキャンバス上の5つの写真を移動したい場合、なぜジェスチャが必要ですか?

もちろん、2番目のケースではWM_TOUCHを自分でブロックすることができますが、誰かがそれを処理し始めてデータを取得したので、コードを再ダビングすることから開発者を救ってください。

それでは、反対側から行きましょう。

1.問題の声明


ただし、当社のソフトウェアは基本的に非常に高度なExcelであり、特定のユーザー(この場合は推定者)を対象としています。 ただし、少し言い直します。ソフトウェアの機能とExcelの間の距離は、MsPaintとAdobe Photoshopの違いとほぼ同じです。
ユーザーは、特定のドキュメントをMsPaintの画像だけでなく、見積もりの​​形式でExcelに実装することもできます。 すべてのcimusは結果です。

このプロジェクトは、WYSIWYGのイデオロギーに従って開発されたもので、90%のケースでは、通常の紙の文書と同様に、ユーザーが作業するグリッドを実装する一種のカスタムクラス(TCustomControlから)です。

これは次のようになります:(スクリーンショットはDragDrop位置の操作中に撮影されたもので、矢印に注意を払わないでください。写真が一部のテクニカルサポートから破れ、チップなどの浮動ヒントを指しているためです。



このコントロールにはスクロールなどの標準的な概念はありません。 もちろん、そうですが、水平方向の移動の場合、または垂直方向の移動の場合-シートの次の行への遷移で、列の操作を操作します。
標準のスクロールメッセージは受け付けません。

(OSが発行する)基本バージョンでは、タッチスクリーンのタップを介してシステムによってエミュレートされるマウスクリックイベントと、タッチを介してシステムによってエミュレートされるWM_MOUSEMOVEを受信できます。

そして必要なもの:


XE4のジェスチャは、マルチタッチでは(ゲストエディターのレベルであっても)基本的にシャープ化されていないが、問題を解決する必要があるという事実を考えると、夜中ずっと悲しくなり、朝になって仕事を始めました。

2.使用される用語


先ほど言ったように、私はこれらの新しいトレンドすべてに精通しているわけではないので、記事では次の定義を使用します(それらは間違っている可能性があります)。

タップ -マウスクリックの類似物。指でタッチスクリーンを1回短くクリックすると発生するイベント。
タッチ (またはタッチポイント)は、指がタッチスクリーンに接触している(およびWM_TOUCHメッセージが処理されている)状況を表すものです。
ルート -ユーザーが指を動かした座標のリスト(動かされたホイールのポイント)。
セッション -指がタッチスクリーンに触れると開始し、ユーザーが指を離さずにその上を移動すると続行し、指を離すと終了します。 セッション全体を通して、そのルートが構築されます。
ジェスチャは、セッションルートと比較されるルートのテンプレートテンプレートです。 たとえば、ユーザーが指をつまんで、左に引いて離しました-これは識別子sgiLeftのジェスチャーです。

3. WM_TOUCH処理について


最初に決定する必要があります-ハードウェアはマルチタッチをサポートしていますか?
これを行うには、SM_DIGITIZERパラメーターを指定してGetSystemMetricsを呼び出し、NID_READYとNID_MULTI_INPUTの2つのフラグの結果を確認します。

大体:

 tData := GetSystemMetrics(SM_DIGITIZER); if tData and NID_READY <> 0 then if tData and NID_MULTI_INPUT <> 0 then ...  ,   

残念ながら、OS Windowsでマルチタッチをサポートするデバイスを使用していない場合、この記事の残りの部分は、結果を確認する可能性のない理論に過ぎません。 Microsoft Surface 2.0 SDKからホイールエミュレーターを使用してみることができますが、試してはいません。

しかし!!! デバイスがマルチタッチをサポートしている場合は、タッチを試みることができます。 これを行うには、任意のウィンドウ(メインフォームなど)を選択して、次のように言います。

 RegisterTouchWindow(Handle, 0); 

この関数を呼び出さないと、選択したウィンドウはWM_TOUCHメッセージを受信しません。
UnregisterTouchWindow関数は、ウィンドウがこのメッセージを受信しないように「切断」するのに役立ちます。

メッセージハンドラーWM_TOUCHを宣言します。

 procedure WmTouch(var Msg: TMessage); message WM_TOUCH; 

そして、私たちは彼が何を私たちに与えてくれるかを理解し始めます。

そのため、このメッセージのWParamパラメーターには、システムが通知する手押し車のアクティブポイントの数が含まれています。 さらに、この数値は下位2バイトにのみ格納されます。これは、システムが最大65535の入力ポイントをサポートする可能性を示唆しています。

私はそれを理解しようとしました-私のモニターは最大10本の指を保持しているため、うまくいきませんでした。 ただし、これは現代の空想科学小説映画を見るとチムスですが、10個すべてを突くことができる多くの人々と連携するデータを含む仮想テーブルを示しています(たとえば、アバターは同じ、または忘却)。

よくやった、彼らは約束をしたが、それが判明したように、それは長い間映画なしで働いてきたが、私は常にニュースに従っていない。 たとえば、Consumer Electronics Show 2011で発表されたこのような46インチデバイスは次のとおりです。



ただし、気が散ることはありません。
ただし、このメッセージのLParamは、GetTouchInputInfo関数を呼び出してメッセージに関する詳細情報を取得できる一種のハンドルです。
GetTouchInputInfoを呼び出した後、この関数の2回目の呼び出しが不要な場合、MSDNはCloseTouchInputHandleを指定することをお勧めしますが、これは必要ありません。 コントロールをDefWindowProcに転送するとき、またはSendMessage / PostMessageを介してデータを送信しようとするとき、ヒープ上のデータのクリアは自動的に行われます。
詳細はこちら

GetTouchInputInfo関数に必要なもの:

  1. 彼女はハンドル自体が必要であり、それと一緒に作業します。
  2. 彼女は、TTouchInput要素の配列の形式で専用のバッファを必要とし、そこにイベントに関するすべての情報を配置します。
  3. この配列のサイズ。
  4. 配列内の各要素のサイズ。

よくやった:4番目の段落の助けを借りて、OSの将来のバージョンでTTouchInputの構造を変更する可能性をすぐに決定しました(さらに面白いことはありますか?

非常に失礼な場合、彼女の呼び出しは次のようになります。

 var Count: Integer; Inputs: array of TTouchInput; begin Count := Msg.WParam and $FFFF; SetLength(Inputs, Count); if GetTouchInputInfo(Msg.LParam, Count, @Inputs[0], SizeOf(TTouchInput)) then // ... -     CloseTouchInputHandle(Msg.LParam); 

以上です。 ここで、Inputs配列に格納されているデータを把握してみましょう。

4. TTouchInputを処理します


この瞬間から、楽しみが始まります。

TTouchInput配列のサイズは、タッチスクリーンに接続されている指の数によって異なります。
手押し車(指)の各ポイントに対して、システムはセッション全体(指を触れてから削除するまで)を通じて変わらない一意のIDを生成します。
このIDは、TTouchInput配列の各要素にマップされ、dwIDパラメーターに格納されます。

セッションといえば:
セッション、これは...さて、このようにしましょう:


写真は(各指に対して)正確に10セッションを示し、そのルート(各セッション内で指が上に移動したポイントの配列)を示し、各セッションはまだ完了していません(指はタッチスクリーンにまだ接続されています)。

ただし、TTouchInput構造に戻ります。
実際、この構造のタコを使用した通常の操作では、いくつかのパラメーターのみが必要です。

 TOUCHINPUT = record x: Integer; //   y: Integer; //   hSource: THandle; //  ,   dwID: DWORD; //    dwFlags: DWORD; //    //       dwMask: DWORD; dwTime: DWORD; dwExtraInfo: ULONG_PTR; cxContact: DWORD; cyContact: DWORD; end; 

デモアプリケーションをすぐに始めましょう。
新しいプロジェクトを作成し、メインフォームにTMemoを配置します。メインフォームには、タコでの作業のログが表示されます。

フォームコンストラクターで、それをWM_TOUCHメッセージの処理に接続します。

 procedure TdlgSimpleTouchDemo.FormCreate(Sender: TObject); begin RegisterTouchWindow(Handle, 0); end; 

次に、イベントハンドラを作成します。

 procedure TdlgSimpleTouchDemo.WmTouch(var Msg: TMessage); function FlagToStr(Value: DWORD): string; begin Result := ''; if Value and TOUCHEVENTF_MOVE <> 0 then Result := Result + 'move '; if Value and TOUCHEVENTF_DOWN <> 0 then Result := Result + 'down '; if Value and TOUCHEVENTF_UP <> 0 then Result := Result + 'up '; if Value and TOUCHEVENTF_INRANGE <> 0 then Result := Result + 'ingange '; if Value and TOUCHEVENTF_PRIMARY <> 0 then Result := Result + 'primary '; if Value and TOUCHEVENTF_NOCOALESCE <> 0 then Result := Result + 'nocoalesce '; if Value and TOUCHEVENTF_PEN <> 0 then Result := Result + 'pen '; if Value and TOUCHEVENTF_PALM <> 0 then Result := Result + 'palm '; Result := Trim(Result); end; var InputsCount, I: Integer; Inputs: array of TTouchInput; begin //     InputsCount := Msg.WParam and $FFFF; //     SetLength(Inputs, InputsCount); //      if GetTouchInputInfo(Msg.LParam, InputsCount, @Inputs[0], SizeOf(TTouchInput)) then begin //   (    ) CloseTouchInputHandle(Msg.LParam); //     for I := 0 to InputsCount - 1 do Memo1.Lines.Add(Format('TouchInput №: %d, ID: %d, flags: %s', [I, Inputs[I].dwID, FlagToStr(Inputs[I].dwFlags)])); end; end; 

以上です。

同意する-不可能にだけ。 あなたの目の前のすべてのデータ。
タッチスクリーンを使用してこのコードを試してみてください。開発者には、各ホイールバーのIDへのバインドに加えて、ログに表示される特定のフラグセットが与えられます。
ログデータによれば、手押し車のセッションの開始(フラグTOUCHEVENTF_DOWN)、タッチスクリーン上の各指の動き(フラグTOUCHEVENTF_MOVE)、およびセッションの終了(フラグTOUCHEVENTF_UP)をすぐに判断できます。

次のようになります。



迷惑行為についてすぐに予約します。タッチスクリーンからTOUCHEVENTF_DOWNまたはTOUCHEVENTF_UPフラグが付いたメッセージがWM_TOUCHハンドラーに届くとは限りません。 「ラッパークラス」を実装するときは、このニュアンスを考慮する必要があります。これについては以下で説明します。

例:
現在、アプリケーションはPopupMenuを表示しています-タッチスクリーンをクリックすると閉じますが、TOUCHEVENTF_DOWNフラグのあるWM_TOUCHメッセージは届きませんが、TOUCHEVENTF_MOVEフラグのある次のメッセージは正常に受信されます。
TOUCHEVENTF_MOVEイベントハンドラーでPopupMenuを表示する場合も同様です。
この場合、セッションは中断され、TOUCHEVENTF_UPフラグが設定されたWM_TOUCHメッセージは予期されません。

この動作は、Windows 7(32/64ビット)でも見られます。Windows8以降では、何かが変更されましたが、今では確認する機会がありません(怠lazが2番目です)。

ただし、「どのように機能するか」というアイデアを得たので、もっと面白いことを書きます。

この例のソースコードは、ソースアーカイブの「 。\ Demos \ simple \ 」フォルダーにあります。

5.実際にマルチタッチを適用します。


私のモニターには同時に10本の指があり、ピアノをエミュレートするアプリケーションを作成することもできます(ただし、ピアノにはペダルと押圧力に対する感度があります)が、なぜ複雑なものからすぐに行くのでしょうか?
私に起こった最も簡単なことは、フォームのキャンバス上の10個の正方形であり、手押し車の助けを借りてすべての方向に移動できます。
これは、最も文字通りの意味でマルチタッチを「感じる」のに十分です。

新しいプロジェクトを作成します。

各正方形は、そのような構造として説明されます。

 type TData = record Color: TColor; ARect, StartRect: TRect; StartPoint: TPoint; Touched: Boolean; TouchID: Integer; end; 

実際、この構造の最も重要なフィールドはTouchIDであり、それ以外はすべてセカンダリです。

各正方形のデータをどこかに保存する必要があるので、そのような配列として宣言しましょう。

 FData: array [0..9] of TData; 

さて、初期化をしましょう:

 procedure TdlgMultiTouchDemo.FormCreate(Sender: TObject); var I: Integer; begin DoubleBuffered := True; RegisterTouchWindow(Handle, 0); Randomize; for I := 0 to 9 do begin FData[I].Color := Random($FFFFFF); FData[I].ARect.Left := Random(ClientWidth - 100); FData[I].ARect.Top := Random(ClientHeight - 100); FData[I].ARect.Right := FData[I].ARect.Left + 100; FData[I].ARect.Bottom := FData[I].ARect.Top + 100; end; end; 

また、フォームのキャンバス上でのレンダリング(今のところ、FormPaintハンドラーを分析せずに、少し下に移動します):

 procedure TdlgMultiTouchDemo.FormPaint(Sender: TObject); var I: Integer; begin Canvas.Brush.Color := Color; Canvas.FillRect(ClientRect); for I := 0 to 9 do begin Canvas.Pen.Color := FData[I].Color xor $FFFFFF; if FData[I].Touched then Canvas.Pen.Width := 4 else Canvas.Pen.Width := 1; Canvas.Brush.Color := FData[I].Color; Canvas.Rectangle(FData[I].ARect); end; end; 

実行すると、次のようになります。



ボディキットの準備ができました。次に、WM_TOUCHの処理を通じて画像を変更してみましょう。

ハンドラーで必要なのは、ユーザーがクリックした正方形のインデックスを取得することです。 しかし、最初に、手押し車の各ポイントからの座標をウィンドウの座標に変換します。

 pt.X := TOUCH_COORD_TO_PIXEL(Inputs[I].x); pt.Y := TOUCH_COORD_TO_PIXEL(Inputs[I].y); pt := ScreenToClient(pt); 

有効な座標を手に入れたら、PtInRectを呼び出すことで、配列内の正方形のインデックスを見つけることができます。

 function GetIndexAtPoint(pt: TPoint): Integer; var I: Integer; begin Result := -1; for I := 0 to 9 do if PtInRect(FData[I].ARect, pt) then begin Result := I; Break; end; end; 

ユーザーが指でタッチスクリーンに触れたとき(各ポイントに固有のIDがある場合)、見つかった四角形にこのIDを割り当てます。 これは将来役に立つでしょう:

 if Inputs[I].dwFlags and TOUCHEVENTF_DOWN <> 0 then begin Index := GetIndexAtPoint(pt); if Index < 0 then Continue; FData[Index].Touched := True; FData[Index].TouchID := Inputs[I].dwID; FData[Index].StartRect := FData[Index].ARect; FData[Index].StartPoint := pt; Continue; end; 

これは、たとえば、オブジェクトの初期化と手押し車のセッションの開始です。

次のメッセージは、TOUCHEVENTF_MOVEフラグが設定されたWM_TOUCHである可能性があります。

ニュアンスがあります:
最初のケースでは、座標で正方形を検索しましたが、フォーム上の正方形の位置が交差する可能性があるため、正方形は間違いになります。
したがって、MOVEの場合、TouchIDパラメーターで設定されたホイールのIDで正方形を検索します。

 function GetIndexFromID(ID: Integer): Integer; var I: Integer; begin Result := -1; for I := 0 to 9 do if FData[I].TouchID = ID then begin Result := I; Break; end; end; 

必要な正方形を見つけたら、タッチセッションの開始時に指定された構造に焦点を当てて移動します。

 R := FData[Index].StartRect; OffsetRect(R, pt.X - FData[Index].StartPoint.X, pt.Y - FData[Index].StartPoint.Y); FData[Index].ARect := R; 


さて、そしてTOUCHEVENTF_UPフラグを処理する形で終わる:

 if Inputs[I].dwFlags and TOUCHEVENTF_UP <> 0 then begin FData[Index].Touched := False; FData[Index].TouchID := -1; Continue; end; 

正方形をタッチセッションから切断し、キャンバス自体を再描画します。

非常に単純な例ですが、これは機能し、お金を要求しません。
実行してテスト-それは非常に面白いことが判明しました:



美しくするために、TData構造体のTouchedパラメーターはFormPaint内で使用され、移動された正方形の周囲に太字のフレームを表示します。

この例のソースコードは、ソースアーカイブの「 。\ Demos \ multutouch \ 」フォルダにあります。

6.ジェスチャ(ジェスチャ)を理解する


マルチタッチは最初のステップに過ぎません。マルチタッチジェスチャを使用したいのですが...
まず、1本のタッチセッション(1本の指)に基づいてVCLでジェスチャ認識がどのように実装されるかを見てみましょう。

TGestureEngineクラスはこれに責任があり、原則としてIsGesture()関数のコードのみが必要です。

もっと詳しく考えてみましょう。

これは正確に2つの部分に分かれており、最初の部分はループ内の標準ジェスチャーをチェックします。

 // Process standard gestures if gtStandard in GestureTypes then 

2番目-ユーザーが送信したカスタムジェスチャ:

 // Process custom gestures if CustomGestureTypes * GestureTypes = CustomGestureTypes then 

定義上、カスタムユーザージェスチャは必要ないため、関数の最初の部分のみを考慮します。
その主なアイデアは、FindStandardGestureを呼び出し、Recognizer.Matchを使用して渡されたルートと比較することにより、ジェスチャー記述子を検索することです。

実際、IsGestureに送られるその他のパラメーターはすべて除外できます。これらはボディキットの機能です。

秘trickは、RecognizerはIGestureRecognizerインターフェイスではなく、VCLラッパーであるということです。
これが必要なものです。

しかし、デモの作成に移る前に、ジェスチャー自体が何であるかを理解する必要があります(Gerture):

これはフォームの構造です:

 TStandardGestureData = record Points: TGesturePointArray; GestureID: TGestureID; Options: TGestureOptions; Deviation: Integer; ErrorMargin: Integer; end; 

ポイントは、ユーザーのタッチセッションからの同様のルートと比較されるジェスチャールートです。
GestureIDは一意のジェスチャー識別子です。

XE4では、Vcl.Controlsモジュールにリストされています。
 const // Standard gesture id's sgiNoGesture = 0; sgiLeft = 1; sgiRight = 2; ... 


オプション -この場合、それらには興味がありません。

DeviationとErrorMarginは、値を示すパラメーターです。たとえば、ジェスチャー中の指の「振戦」です。 Y軸上の位置を変更せずに、X軸に沿って完全に平らな線を左に描画できる可能性は低いため、DeviationとErrorMarginは、ポイントの移動が有効な境界を示します。

標準のジェスチャーパラメーターの宣言は、Vcl.Touch.Gesturesモジュールにあります。
 { Standard gesture definitions } const PDefaultLeft: array[0..1] of TPoint = ((X:200; Y:0), (X:0; Y:0)); CDefaultLeft: TStandardGestureData = ( GestureID: sgiLeft; Options: [goUniDirectional]; Deviation: 30; ErrorMargin: 20); PDefaultRight: array[0..1] of TPoint = ((X:0; Y:0), (X:200; Y:0)); CDefaultRight: TStandardGestureData = ( GestureID: sgiRight; Options: [goUniDirectional]; Deviation: 30; ErrorMargin: 20); PDefaultUp: array[0..1] of TPoint = ((X:0; Y:200), (X:0; Y:0)); CDefaultUp: TStandardGestureData = ( GestureID: sgiUp; Options: [goUniDirectional]; Deviation: 30; ErrorMargin: 20); ... 


したがって、ジェスチャの形式を知っていると、ルート(ポイント)に入力して一意のIDを設定することで、実行時に独自のバージョンのジェスチャを個別に準備できます。
ただし、今は必要ありません。 標準のジェスチャーに基づいて何ができるかを見てみましょう。

Recognizerが認識したジェスチャのIDを返すことで、最も簡単な例を作成しています。ここでは、ユーザーがタッチスクリーンから入力するルートに技術的に類似した4つのポイントの配列を作成します。

たとえば、次のように:
 program recognizer_demo; {$APPTYPE CONSOLE} {$R *.res} uses Windows, Vcl.Controls, SysUtils, TypInfo, Vcl.Touch.Gestures; type TPointArray = array of TPoint; function GetGestureID(Value: TPointArray): Byte; var Recognizer: TGestureRecognizer; GestureID: Integer; Data: TStandardGestureData; Weight, TempWeight: Single; begin Weight := 0; Result := sgiNone; Recognizer := TGestureRecognizer.Create; try for GestureID := sgiLeft to sgiDown do begin FindStandardGesture(GestureID, Data); TempWeight := Recognizer.Match(Value, Data.Points, Data.Options, GestureID, Data.Deviation, Data.ErrorMargin); if TempWeight > Weight then begin Weight := TempWeight; Result := GestureID; end; end; finally Recognizer.Free; end; end; const gesture_id: array [sgiNone..sgiDown] of string = ( 'sgiNone', 'sgiLeft', 'sgiRight', 'sgiUp', 'sgiDown' ); var I: Integer; Data: TPointArray; begin SetLength(Data, 11); //     for I := 0 to 10 do begin Data[I].X := I * 10; Data[I].Y := 0; end; Writeln(gesture_id[GetGestureID(Data)]); //     for I := 0 to 10 do begin Data[I].X := 500 - I * 10; Data[I].Y := 0; end; Writeln(gesture_id[GetGestureID(Data)]); //     for I := 0 to 10 do begin Data[I].X := 0; Data[I].Y := 500 - I * 10; end; Writeln(gesture_id[GetGestureID(Data)]); //     for I := 0 to 10 do begin Data[I].X := 0; Data[I].Y := I * 10; end; Writeln(gesture_id[GetGestureID(Data)]); Readln; end. 


開始後、次の図が表示されます。



予想通り。
この例のソースコードは、ソースを含むアーカイブ内のフォルダー「 。\ Demos \認識機能 」にあります。

そして今...

7.マルチタッチジェスチャ(ジェスチャ)を認識する


この章では、この記事の主なアイデアについて説明します。このように言えば、このテキストがすべて載っているチップです。
今-技術的な詳細はなく、アプローチ自体のみ:

したがって、現在利用可能なものは次のとおりです。
  1. 各タッチセッションからデータを取得する方法を知っています。
  2. 各タッチセッションのジェスチャを認識できます。

例:
  1. ユーザーはタッチスクリーンをクリックして左にスワイプしました。
  2. ON_TOUCH + TOUCHEVENTF_DOWNハンドラーでセッションの開始を記録し、TOUCHEVENTF_MOVEの到着時にすべてのウェイポイントを記録し、TOUCHEVENTF_UPを受け取った瞬間に、以前に記録したポイント配列をGetGestureID関数に渡しました。
  3. 結果を導き出しました。

しかし、ユーザーが一度に2本の指で同じことをしたと想像してください。
  1. 各指について、独自のセッションを開始します。
  2. 彼女のルートを書きます。
  3. 各セッションの終わりに、ジェスチャ認識に転送します。

同じウィンドウで実行された2つのセッションのジェスチャIDが一致する場合(たとえば、sgiLeft)、結論を出すことができます-2本の指で左にスワイプしました。

しかし、セッションルート上のすべてのポイントに同じ座標が含まれている場合はどうでしょうか?
その後、ジェスチャーはなく、いわゆるタップ(1本または複数の指)が発生しました。
さらに、PopupMenuが通常表示される「プレスアンドタップ」ジェスチャもこの条件に該当します。

したがって、問題の主な定式化を考慮して、必要なすべてのジェスチャを1本、2本、3本の指で制御できます(ただし、少なくとも10本すべて)。

また、2つのセッションのジェスチャーが一致しなかった場合はどうなりますか?
それらを分析するには、これは問題の現在のステートメントには含まれていませんが、最初のセッションのsgiLeftジェスチャーと2番目のセッションのsgiRightジェスチャーをズームとして解釈できると言っても安全です。2回のタッチセッションのみに基づくsgiSemiCircleLeftまたはsgiSemiCircleRightジェスチャに基づいて、Rotateでさえ検出することができます。

貫通?

この方法で簡単にエミュレートできるジェスチャの既定のリストを次に示します
。Windowsタッチジェスチャの概要

残念ながら、何らかの理由で、これらはすべてXE4に実装されておらず、第7バージョンからのみ利用可能になりました(完全に確実かどうかはわかりません)。

8.エンジンの技術計画


理論部分が終了したら、今度はこれらすべてを実践し、開発者が直面しているいくつかの問題をすぐに検討します。

問題の回数:
通常、アプリケーションには数百のウィンドウがあります。ほとんどの場合、システムはWM_LBUTTONCLICKプランメッセージなどを生成し、ウィンドウの通常の動作(ボタン、編集、スクロールなど)には十分ですが、同じSysListView32には十分ですWM_SCROLLメッセージが生成されないため、2本の指でのジェスチャーによるスクロールは発生しません。しかし、カスタムコントロールもあります。
各ウィンドウのウィンドウプロシージャを拡張するのは非常に手間がかかるため、何らかの方法で決定する必要があります。どのウィンドウがマルチタッチをサポートする必要があり、これを最も普遍的に行う必要があります。
それは次のとおりです。ウィンドウが登録され、マルチタッチのすべての作業を担当する特定のマルチタッチマネージャーが必要です。

問題点2:
TWinControlの各インスタンスを書き換えずに少し普遍的に記述しているため、RecreateWndは通常のVCLメカニズムの1つを呼び出すため、ウィンドウの再作成を何らかの方法で追跡する必要があります。これを行わないと、最初にウィンドウを再作成するときに、以前に登録したTWinControlがWM_TOUCHメッセージの受信を停止するため、マネージャーのすべての作業が平準化されます。

問題番号3:
マネージャーは、タッチセッションに関するすべてのデータを保存し、セッションの開始と終了の中断の状況を処理できる必要があります(ダウンフラグとアップフラグの通知が常に送信されるとは限らないため)。また、セッションの長さが非常に長くなる可能性があることに注意してくださいセッションのルートのすべてのポイントを保存する場合のメモリ消費。

また、マルチタッチマネージャーが異なるウィンドウ内のジェスチャを区別できるようにしたいと思います。
たとえば、ユーザーが左のウィンドウに2本の指を置き、右に2本の指を置いて(4つのマルチタッチセッション)、中央に指を接続すると、通知は右に2本指のジェスチャーを、左に2本指のジェスチャーを受け取るはずです。



しかし、残念ながら、これは機能しません。WM_TOUCHメッセージは、セッションが開始されたウィンドウにのみ届き、残りのウィンドウは無視されます。

9.マルチタッチエンジンのベースフレームを構築する


最初に、クラス実装のニュアンスを決定しましょう。
技術的には、外部プログラマーの観点から最も便利なのは、すべての作業を引き受け、最終イベントの呼び出しを除いて開発者に通知するユニバーサルエンジンの実装です。

この場合、開発者は必要なウィンドウを一度エンジンに登録し、そこからのジェスチャーを分析して(特定のウィンドウに向けて)必要なものを処理するだけです。たとえば、2本指のジェスチャーで同じスクロールをエミュレートします。

エンジン自体はシングルトンとして実装されます。
第一に、常に同じことを行うクラスインスタンスを作成しても意味がありません。これは、データストレージ用に強化されたTStringListではなく、すべてのプロジェクトウィンドウに対して単一の作業ロジックを実装するエンジンです。
次に、エンジン自体の実装にはわずかなニュアンスがあります(それについては少し後で)。そのため、シングルトン形式の実装が最も簡単になります。そうでない場合は、クラスのロジックを根本的に再複雑化する必要があります。

したがって、エンジンは以下を提供する必要があります。
  1. ウィンドウを登録し、登録からウィンドウを削除する方法:
  2. 開発者がハンドラーを実装する必要がある外部イベントのセット。

外部イベントは次のようなものです

。OnBeginTouch-このイベントは、WM_TOUCHメッセージを受信したときに発生します。

説明させてください:4番目の章では、次のコードが与えられました。

 //     InputsCount := Msg.WParam and $FFFF; 

つまり手押し車のいくつかの実際のポイントがあります。
開発者に警告するのは、その数についてです。

OnTouch-このイベントでは、各TTouchInput構造に含まれるデータを開発者に通知しますが、もう少しコーミングします。 (ポイントに関するデータをウィンドウ座標に変換し、正しいフラグを設定するなど、なぜ開発者に冗長な情報をロードし、冗長なコードを書かせるのですか?)

OnEndTouch-これにより、WM_TOUCHメッセージ処理サイクルが完了したと表示されます。

OnGecture-ジェスチャが認識されたとエンジンが判断すると、開発者はこのメッセージを受け取ります。

クラスはシングルトンとして実装され、その中に複数のウィンドウが登録されるため、4つのイベントすべてをクラスプロパティとして宣言することはできません。

正確に言うと、それはもちろん可能ですが、2番目に登録されたウィンドウはすぐにイベントハンドラーを自分自身に再割り当てし、最初のウィンドウは静かに吸わなければなりません。
したがって、登録されたウィンドウのリストに加えて、それらに割り当てられたエンジンイベントハンドラを保持する必要があります。

ただし、ここですべてを実行しようとします。
新しいプロジェクトを作成し、SimpleMultiTouchEngineなどの名前の新しいモジュールを追加します。

最初に、WM_TOUCHを処理するときに関心のあるフラグを宣言します。

 type TTouchFlag = ( tfMove, //   tfDown, //    tfUp //     ); TTouchFlags = set of TTouchFlag; 

各ポイントについて外部開発者に渡す構造を説明します。

 TTouchData = record Index: Integer; //      TTouchInput ID: DWORD; //  ID  Position: TPoint; //     Flags: TTouchFlags; //  end; 

OnTouchBeginイベントの宣言は次のようになります。

 TTouchBeginEvent = procedure(Sender: TObject; nCount: Integer) of object; 

そして、これがOnTouchの外観です。

 TTouchEvent = procedure(Sender: TObject; Control: TWinControl; TouchData: TTouchData) of object; 

OnEndTouchの場合、通常のTNotifyEventで十分です。

登録された各ウィンドウに割り当てられた割り当てられたイベントハンドラのデータは、次の構造に格納されます。

 TTouchHandlers = record BeginTouch: TTouchBeginEvent; Touch: TTouchEvent; EndTouch: TNotifyEvent; end; 

新しいクラスを宣言します:

  TSimleMultiTouchEngine = class private const MaxFingerCount = 10; private type TWindowData = record Control: TWinControl; Handlers: TTouchHandlers; end; private FWindows: TList<TWindowData>; FMultiTouchPresent: Boolean; protected procedure DoBeginTouch(Value: TTouchBeginEvent; nCount: Integer); virtual; procedure DoTouch(Control: TWinControl; Value: TTouchEvent; TouchData: TTouchData); virtual; procedure DoEndTouch(Value: TNotifyEvent); virtual; protected procedure HandleTouch(Index: Integer; Msg: PMsg); procedure HandleMessage(Msg: PMsg); public constructor Create; destructor Destroy; override; procedure RegisterWindow(Value: TWinControl; Handlers: TTouchHandlers); procedure UnRegisterWindow(Value: TWinControl); end; 

順番に:

MaxFingerCount定数には、クラスで使用できる手押し車の最大数が含まれています。

構造TWindowData-登録されたウィンドウと、プログラマーが割り当てたハンドラーのリストが含まれます。

フィールドFWindows:TList-登録されたウィンドウとハンドラーのリストで、そこからクラスでの作業を通して踊ります。

FMultiTouchPresentフィールドは、クラスのコンストラクターで初期化されるフラグです。
ハードウェアがマルチタッチを保持している場合はTrueが含まれます。このフラグに基づいて、クラスのロジックの一部が無効になります(まだ実行できないのに、なぜ追加のジェスチャーを行うのですか?)。

最初の保護されたセクション-便宜上、外部イベントへのすべての呼び出しが行われます。

HandleTouchプロシージャはエンジンのコアエンジンであり、WM_TOUCHメッセージの処理を担当するのは彼女です。

HandleMessageプロシージャは補助です。そのタスクは、登録されているウィンドウのどれにメッセージが送信されたかを判別し、HandleTouchを呼び出して、見つかったウィンドウのインデックスを渡すことです。

パブリックセクション-コンストラクタ、デストラクタ、ウィンドウの登録および登録解除。

クラスの実装を進める前に、すぐにシングルトンキットを作成します。
  function MultiTouchEngine: TSimleMultiTouchEngine; implementation var _MultiTouchEngine: TSimleMultiTouchEngine = nil; function MultiTouchEngine: TSimleMultiTouchEngine; begin if _MultiTouchEngine = nil then _MultiTouchEngine := TSimleMultiTouchEngine.Create; Result := _MultiTouchEngine; end; ... initialization finalization _MultiTouchEngine.Free; end. 


そして、すべての最後に、エンジンに登録されているウィンドウに送信されたWM_TOUCHメッセージを受信するコールバックトラップ。
 var FHook: HHOOK = 0; function GetMsgProc(nCode: Integer; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall; begin if (nCode = HC_ACTION) and (WParam = PM_REMOVE) then if PMsg(LParam)^.message = WM_TOUCH then MultiTouchEngine.HandleMessage(PMsg(LParam)); Result := CallNextHookEx(FHook, nCode, WParam, LParam); end; 


念のため、使用されるモジュールのリストは次のようになります。
 uses Windows, Messages, Classes, Controls, Generics.Defaults, Generics.Collections, Vcl.Touch.Gestures; 


それでは、エンジン自体の実装について見ていきましょう。コンストラクターから始めましょう。

 constructor TSimleMultiTouchEngine.Create; var Data: Integer; begin // ,     Data := GetSystemMetrics(SM_DIGITIZER); FMultiTouchPresent := (Data and NID_READY <> 0) and (Data and NID_MULTI_INPUT <> 0); //  ,       if not FMultiTouchPresent then Exit; //         FWindows := TList<TWindowData>.Create( //   IndexOf          Control //    TComparer<twindowdata>.Construct( function (const A, B: TWindowData): Integer begin Result := Integer(A.Control) - Integer(B.Control); end) ); end; 

飾り気のないかなりシンプルなデザイナー、コメントですべてのステップを見ることができます。
ただし、デストラクタも簡単です。

 destructor TSimleMultiTouchEngine.Destroy; begin if FHook <> 0 then UnhookWindowsHookEx(FHook); FWindows.Free; inherited; end; 

デストラクタの唯一のニュアンスは、トラップが以前にインストールされていた場合、その除去です。
次に、開発者が外部から利用できる2つの固有のパブリックプロシージャの実装に移りましょう。

エンジンでのウィンドウ登録:

 procedure TSimleMultiTouchEngine.RegisterWindow(Value: TWinControl; Handlers: TTouchHandlers); var WindowData: TWindowData; begin //     -  if not FMultiTouchPresent then Exit; //    IndexOf ,     WindowData.Control := Value; //      , //     if FWindows.IndexOf(WindowData) < 0 then begin //    WindowData.Handlers := Handlers; //     RegisterTouchWindow(Value.Handle, 0); //       FWindows.Add(WindowData); end; //      if FHook = 0 then FHook := SetWindowsHookEx(WH_GETMESSAGE, @GetMsgProc, HInstance, GetCurrentThreadId); end; 

ただし、IndexOfを呼び出す場合の唯一のニュアンスはすべてコメントされます。 CompareMemが2つの構造を相互に比較するのではなく、構造の1つのフィールド(Control)のみに対して機能するように、TComparerはリストクラスのコンストラクタに実装されました。

コードからわかるように、ロジックは単純です。ウィンドウを一般リストに追加すると、クラスはWH_GETMESSAGEトラップを開始し(以前に実行されていない場合)、現在のスレッド内でのみ機能します。

変数FMultiTouchPresentについては別に説明します。
コードからわかるように、それは単にヒューズとして機能し、有用なことができない場合はクラスのロジック全体を無効にします。
削除すると、ハードウェアにタッチスクリーンがまったくない場合のトラップの設定により、アプリケーションの各ウィンドウのメッセージ取得サイクルにわずかな「オーバーヘッド」が発生します。必要ですか?

登録からウィンドウを削除することも同じ原理に従い、ウィンドウがもうない場合はトラップがオフになります。

 procedure TSimleMultiTouchEngine.UnRegisterWindow(Value: TWinControl); var Index: Integer; WindowData: TWindowData; begin //     -  if not FMultiTouchPresent then Exit; //    IndexOf ,     WindowData.Control := Value; //   Index := FWindows.IndexOf(WindowData); if Index >= 0 then //  ,     FWindows.Delete(Index); //    ,       if FWindows.Count = 0 then begin //   UnhookWindowsHookEx(FHook); FHook := 0; end; end; 

実際、エンジンのロジック全体は単純です。登録のウィンドウを受け入れ、WM_TOUCHメッセージを受信すると、クラスのシングルトンにアクセスしてHandleMessageプロシージャを呼び出すトラップを起動しました。

 procedure TSimleMultiTouchEngine.HandleMessage(Msg: PMsg); var I: Integer; begin for I := 0 to FWindows.Count - 1 do //   ,    if FWindows[I].Control.Handle = Msg^.hwnd then begin //      HandleTouch(I, Msg); Break; end; end; 

そしてここに、作業の論理全体が展開するクラスの中心的な手順があります。

 procedure TSimleMultiTouchEngine.HandleTouch(Index: Integer; Msg: PMsg); var TouchData: TTouchData; I, InputsCount: Integer; Inputs: array of TTouchInput; Flags: DWORD; begin // ,      InputsCount := Msg^.wParam and $FFFF; if InputsCount = 0 then Exit; //          if InputsCount > MaxFingerCount then InputsCount := MaxFingerCount; //       SetLength(Inputs, InputsCount); if not GetTouchInputInfo(Msg^.LParam, InputsCount, @Inputs[0], SizeOf(TTouchInput)) then Exit; CloseTouchInputHandle(Msg^.LParam); //       //       DoBeginTouch(FWindows[Index].Handlers.BeginTouch, InputsCount); for I := 0 to InputsCount - 1 do begin TouchData.Index := I; //      ID   //        ( Down  Up) //       TouchData.ID := Inputs[I].dwID; //        TouchData.Position.X := TOUCH_COORD_TO_PIXEL(Inputs[I].x); TouchData.Position.Y := TOUCH_COORD_TO_PIXEL(Inputs[I].y); TouchData.Position := FWindows[Index].Control.ScreenToClient(TouchData.Position); //    TouchData.Flags := []; Flags := Inputs[I].dwFlags; if Flags and TOUCHEVENTF_MOVE <> 0 then Include(TouchData.Flags, tfMove); if Flags and TOUCHEVENTF_DOWN <> 0 then Include(TouchData.Flags, tfDown); if Flags and TOUCHEVENTF_UP <> 0 then Include(TouchData.Flags, tfUp); //           DoTouch(FWindows[Index].Control, FWindows[Index].Handlers.Touch, TouchData); end; //       //       DoEndTouch(FWindows[Index].Handlers.EndTouch); end; 

これについては記事の第5章ですでに説明しているため、コードについてさらに説明することは意味がありません。結果として得られるマルチタッチエンジンの操作に移りましょう。ソースアーカイブ

の「。\ Demos \ multitouch_engine_demo \フォルダーにあるSimleMultiTouchEngine.pasモジュールのソースコード

10. TSimleMultiTouchEngineの使用


新しいものを思い付くことはせず、5番目の章からプロジェクトを再現します。主な変更点は、TSimleMultiTouchEngineがマルチタッチサポートを提供することです。

第9章で作成したプロジェクトで、第5章のTData構造体とFData配列の宣言を追加し、FormPaintハンドラーをコピーします。これはすべて変更されません。

2つのハンドラーを宣言します。

 procedure OnTouch(Sender: TObject; Control: TWinControl; TouchData: TTouchData); procedure OnTouchEnd(Sender: TObject); 

使用するモジュールで、SimleMultiTouchEngineを接続し、クラスコンストラクターをわずかに変更します。

 procedure TdlgMultiTouchEngineDemo.FormCreate(Sender: TObject); var I: Integer; Handlers: TTouchHandlers; begin DoubleBuffered := True; // RegisterTouchWindow(Handle, 0); Randomize; for I := 0 to 9 do begin FData[I].Color := Random($FFFFFF); FData[I].ARect.Left := Random(ClientWidth - 100); FData[I].ARect.Top := Random(ClientHeight - 100); FData[I].ARect.Right := FData[I].ARect.Left + 100; FData[I].ARect.Bottom := FData[I].ARect.Top + 100; end; ZeroMemory(@Handlers, SizeOf(TTouchHandlers)); Handlers.Touch := OnTouch; Handlers.EndTouch := OnTouchEnd; MultiTouchEngine.RegisterWindow(Self, Handlers); end; 

実際、変更は最小限であり、RegisterTouchWindowを呼び出す代わりに、実装したばかりのMultiTouchEngineに作業をシフトします。

OnTouchEndハンドラーは簡単です。

 procedure TdlgMultiTouchEngineDemo.OnTouchEnd(Sender: TObject); begin Repaint; end; 

キャンバス全体を再描画するだけです。

ここで、OnTouchハンドラーのコード(以前はWmTouchハンドラーに実装されていた)が何に変わったかを見てみましょう。

 procedure TdlgMultiTouchEngineDemo.OnTouch(Sender: TObject; Control: TWinControl; TouchData: TTouchData); function GetIndexAtPoint(pt: TPoint): Integer; var I: Integer; begin Result := -1; for I := 0 to 9 do if PtInRect(FData[I].ARect, pt) then begin Result := I; Break; end; end; function GetIndexFromID(ID: Integer): Integer; var I: Integer; begin Result := -1; for I := 0 to 9 do if FData[I].TouchID = ID then begin Result := I; Break; end; end; var Index: Integer; R: TRect; begin if tfDown in TouchData.Flags then begin Index := GetIndexAtPoint(TouchData.Position); if Index < 0 then Exit; FData[Index].Touched := True; FData[Index].TouchID := TouchData.ID; FData[Index].StartRect := FData[Index].ARect; FData[Index].StartPoint := TouchData.Position; Exit; end; Index := GetIndexFromID(TouchData.ID); if Index < 0 then Exit; if tfUp in TouchData.Flags then begin FData[Index].Touched := False; FData[Index].TouchID := -1; Exit; end; if not (tfMove in TouchData.Flags) then Exit; if not FData[Index].Touched then Exit; R := FData[Index].StartRect; OffsetRect(R, TouchData.Position.X - FData[Index].StartPoint.X, TouchData.Position.Y - FData[Index].StartPoint.Y); FData[Index].ARect := R; end; 


イデオロギーはあまり変化していませんが、古いバージョンよりもはるかに読みやすくなっています。
そして最も重要なのは、第5章のコードと同じように機能することです。


サンプルのソースコードは、ソースアーカイブ内のフォルダー " 。\ Demos \ multitouch_engine_demo \ "にあります。

それで、同じキムスとは何ですか、おそらくあなたは尋ねます。結局のところ、メインフォームのコードのサイズとその操作のアルゴリズムは実際には変更されず、さらに、追加のモジュールがSimleMultiTouchEngine.pasの形式で277行のコード(コメント付き)として現れました。
そのままにして、本当に必要な場合にのみWM_TOUCHハンドラーを自分で実装する方が簡単かもしれません。

原則として、これがその方法です-このエンジンは、第8章で述べた3つの最初の問題のみを解決しますが、本当です。

そして、シムスは次の通りです...

11.エンジンにジェスチャーのサポートを含めます


上記で実装されたMultiTouchEngineには、計画された問題の残りの2つのポイントに対する解決策はありません。それなしでは、プロジェクト階層の単なる追加クラスになります(もちろん、このクラスはすべての苦しんでいる人にマルチタッチを提供できますが、本質は変わりません)。

問題3からすぐに始めましょう。

まず、ジェスチャーエンジンと外部イベントハンドラーによって認識される型を宣言します。

 //    TGestureType = ( gtNone, //    gtTap, gt2Tap, gt3Tap, //   (1, 2, 3 ) gtLeft, gtRight, gtUp, gtDown, //      gt2Left, gt2Right, gt2Up, gt2Down, //      gt3Left, gt3Right, gt3Up, gt3Down //      ); //     TGestureEvent = procedure(Sender: TObject; Control: TWinControl; GestureType: TGestureType; Position: TPoint; Completed: Boolean) of object; 

このクラスでは、15種類のジェスチャーを認識できる必要があります(gtNoneを除く)。

TGestureEvent宣言のCompletedパラメーターに注意してください。このフラグは、ジェスチャの完了を開発者に通知します(WM_TOUCH + TOUCHEVENTF_UPメッセージが到着します)。

これが行われる理由:たとえば、ユーザーが2本の指でタッチスクリーンをクリックして左に移動した場合、原則としてウィンドウをスクロールする必要がありますが、ジェスチャが完了するのを待つと正しく動作しないため、マルチタッチエンジンは定期的に外部OnGestureイベントを生成し、その中で発生することができます手押し車のセッション中に必要な右スクロール。開発者はこのハンドラーで、ジェスチャーが完了したかどうかをCompletedパラメーターによって理解できます(たとえば、gtTapが表示され、CompletedパラメーターがFalseに設定されている場合、何もする必要はなく、終了を待つ価値があります)。

セッション中にOnGestureイベントが生成される頻度は、10に設定したGesturePartSize定数によって異なります。つまり、セッションポイントの数が定数の倍数になると(除算の剰余はゼロになります)、イベントが生成されます。

各セッションのデータは、この配列に保存されます:

 TPointArray = array of TPoint; 

さて、次のように各セッションを記述する構造を宣言します。

  TGestureItem = record ID, // ID ,     ControlIndex: Integer; //  ,      Data: TList<TPoint>; //  ,        Done: Boolean; //      end; 

おそらく、各タッチセッションのデータを格納するクラスを宣言することは引き続き可能です。
 // ,         //   10  TGesturesData = class ... strict private //      FData: array [0..MaxFingerCount - 1] of TGestureItem; ... public ... //   procedure StartGesture(ID, ControlIndex: Integer; Value: TPoint); //      function AddPoint(ID: Integer; Value: TPoint): Boolean; //   procedure EndGesture(ID: Integer); //         procedure ClearControlGestures(ControlIndex: Integer); //         function GetGesturePath(ID: Integer): TPointArray; //        OnEndAllGestures  OnPartComplete property LastControlIndex: Integer read FLastControlIndex; //         LastControlIndex property OnEndAllGestures: TGesturesDataEvent read FEndAll write FEndAll; //     GesturePartSize       LastControlIndex property OnPartComplete: TGesturesDataEvent read FPart write FPart; end; 


これは実際には特別なフリルを含まない非常に単純なクラスであるため、記事に沿ったデモプロジェクトの例ですべてを見ることができるため、各関数の実装については検討しません

彼の全仕事は:
  1. StartGestureおよびAddPoint呼び出しを通じて外部からのデータを保存します。
  2. AddPointを呼び出すたびに、リストのサイズを確認します
     データ:TList <TPoint> 
    ControlIndexウィンドウに関連付けられたセッションごとに、必要に応じてOnPartCompleteを呼び出します。
  3. EndGestureを呼び出した後、同じControlIndexを持つすべてのセッションを確認し、すべて完了した場合は、OnEndAllGesturesを呼び出します。

これはエンジンの単なるセッションリポジトリであり、TGestureRecognizerは保存されているデータを処理します。

次の2つのフィールドを追加して、基本クラスを拡張します。
 //         FGesturesData: TGesturesData; //        FGestureRecognizer: TGestureRecognizer; 


コンストラクターで、セッションリポジトリを作成して初期化します。
 FGesturesData := TGesturesData.Create; FGesturesData.OnEndAllGestures := OnEndAllGestures; FGesturesData.OnPartComplete := OnPartComplete; FGestureRecognizer := TGestureRecognizer.Create; 


その後、HandleTouch()メソッドに戻ります。ここで、TouchData構造体のフラグを設定するコードを少し拡張する必要があります。

 TouchData.Flags := []; Flags := Inputs[I].dwFlags; if Flags and TOUCHEVENTF_MOVE <> 0 then begin Include(TouchData.Flags, tfMove); //    ,    //  ,     if not FGesturesData.AddPoint(TouchData.ID, TouchData.Position) then //      ,    FGesturesData.StartGesture(TouchData.ID, Index, TouchData.Position); end; if Flags and TOUCHEVENTF_DOWN <> 0 then begin Include(TouchData.Flags, tfDown); //      , //      ID FGesturesData.StartGesture(TouchData.ID, Index, TouchData.Position); end; if Flags and TOUCHEVENTF_UP <> 0 then begin Include(TouchData.Flags, tfUp); //         // -    . //        , //  FGesturesData        FGesturesData.EndGesture(TouchData.ID); end; 

実際、これは各セッションのデータウェアハウスでのほぼすべてのアクティブな作業です。

タッチセッションの完了と部分的な完了のイベントハンドラーは非常に簡単です。
 // //             //  Values  ID  , //         // ============================================================================= procedure TTouchManager.OnPartComplete(Values: TBytes); var Position: TPoint; GestureType: TGestureType; begin //       ? GestureType := RecognizeGestures(Values, Position); //   ,     if GestureType <> gtNone then DoGesture( FWindows[FGesturesData.LastControlIndex].Control, FWindows[FGesturesData.LastControlIndex].Handlers.Gesture, GestureType, Position, //            False); end; 

, :

 // //           //  Values  ID    // ============================================================================= procedure TTouchManager.OnEndAllGestures(Values: TBytes); var Position: TPoint; GestureType: TGestureType; begin try //       ? GestureType := RecognizeGestures(Values, Position); //   ,     if GestureType <> gtNone then DoGesture( FWindows[FGesturesData.LastControlIndex].Control, FWindows[FGesturesData.LastControlIndex].Handlers.Gesture, GestureType, Position, //       True); finally //        FGesturesData.ClearControlGestures(FGesturesData.LastControlIndex); end; end; 

, , , .

ハンドラーコードからわかるように、すべての主要な作業はRecognizeGestures関数で行われます。そのロジックについては、第7章で既に説明しました。

次のようになります。

 // //          TGesturesData //  Values  ID  , //         // ============================================================================= function TTouchManager.RecognizeGestures(Values: TBytes; var Position: TPoint): TGestureType; var I, A, ValueLen, GestureLen: Integer; GestureID: Byte; GesturePath: TPointArray; NoMove: Boolean; begin Result := gtNone; //   ,      ValueLen := Length(Values); //     (    ),   if ValueLen > 3 then Exit; //   : //    ID     ( GetGestureID), //      sgiLeft //       ,      ID    , //    -        //          //          //         , //            GestureID := sgiNoGesture; NoMove := True; for I := 0 to ValueLen - 1 do begin // ,       TPoint GesturePath := FGesturesData.GetGesturePath(Values[I]); GestureLen := Length(GesturePath); //      - ,       if GestureLen = 0 then Exit; //       if NoMove then for A := 1 to GestureLen - 1 do if GesturePath[0] <> GesturePath[A] then begin NoMove := False; Break; end; //   . //     .      ,   //     ,           Position := GesturePath[GestureLen - 1]; //  ID     if I = 0 then GestureID := GetGestureID(GesturePath) else //   ID     ,    if GestureID <> GetGestureID(GesturePath) then Exit; end; //     ID         if (GestureID = sgiNoGesture) then begin if NoMove then case ValueLen of 1: Result := gtTap; 2: Result := gt2Tap; 3: Result := gt3Tap; end; end else begin Dec(ValueLen); Result := TGestureType(3 + GestureID + ValueLen * 4); end; end; 

この関数には、補助的なGetGestureIDが必要です。これについては、第6章で既に説明しました。

これらすべての操作の後、第8章で表明された問題番号3は解決されたと言えます。つまり、各セッションに関するデータを保存でき、さらに、どのウィンドウで保持されているかを把握できます。

問題は2つ目です。

12.ウィンドウの再作成を検出します


先ほど言ったように、RecreateWndの呼び出しは基本的に通常のVCLメカニズムです。
ただし、エンジンのロジック全体を大きく損なう可能性があります。ウィンドウを再作成すると、これまでのところ、新しく作成されたハンドルでRegisterTouchWindowを2回目に呼び出す人はいません。したがって、ウィンドウがエンジンに登録され続けたとしても、WM_TOUCHメッセージは送信されなくなります。

このタスクにアプローチする方法はいくつかあります。たとえば、トラップを設定したので、WM_CREATE / WM_DESTROYメッセージをWM_TOUCHヒープにキャッチしてみませんか?

しかし、GUIストリーム内にはこのようなメッセージが多数あるため、メッセージフェッチサイクルに不要なオーバーヘッドが必要な理由はありません。

したがって、反対側に移動して、特定のプロキシを作成します。このプロキシは、非表示ウィンドウになります。このプロキシに対して、親がウィンドウを設定します。この場合、メインウィンドウが破棄されると、プロキシのウィンドウも破棄されます。これは、DestroyHandleハンドラーで検出できます。また、有効な親ハンドルにアクセスできるCreateWndで破棄後のウィンドウ作成を理解します。 WM_TOUCHメッセージの受信。

この不名誉は次のようになります。

 type //       TWinControlProxy = class(TWinControl) protected procedure DestroyHandle; override; procedure CreateWnd; override; procedure CreateParams(var Params: TCreateParams); override; end; { TWinControlProxy } // //     WS_EX_TRANSPARENT,    . // ============================================================================= procedure TWinControlProxy.CreateParams(var Params: TCreateParams); begin inherited; Params.ExStyle := Params.ExStyle or WS_EX_TRANSPARENT; end; // //         // ============================================================================= procedure TWinControlProxy.CreateWnd; begin inherited CreateWnd; if Parent.HandleAllocated then RegisterTouchWindow(Parent.Handle, 0); Visible := False; end; // //      ,    // ============================================================================= procedure TWinControlProxy.DestroyHandle; begin if Parent.HandleAllocated then UnregisterTouchWindow(Parent.Handle); Visible := True; inherited DestroyHandle; end; 


このプロキシはエンジンについて何も知らず、サイレントグリッチはたった1つのタスクを実行します-ウィンドウがタッチスクリーンから切断されないようにするためです。

プロキシをサポートするには、ウィンドウに関連付けられたプロキシへのリンクを追加して、TWindowData構造をわずかに拡張する必要があります。

 TWindowData = record Control, Proxy: TWinControl; 

次に、ウィンドウの登録手順をわずかに変更します。

 if FWindows.IndexOf(WindowData) < 0 then begin //   , //           WindowData.Proxy := TWinControlProxy.Create(Value); WindowData.Proxy.Parent := Value; //   WindowData.Handlers := Handlers; WindowData.LastClickTime := 0; //     RegisterTouchWindow(Value.Handle, 0); FWindows.Add(WindowData); end; 

登録からウィンドウを削除する:

 if Index >= 0 then begin //   ,        FWindows[Index].Proxy.Free; //   //      FWindows.Delete(Index); end; 

それだけです。
仕組みを見てみましょう。

13.マルチタッチエンジンの制御テスト


繰り返しますが、新しいプロジェクトを作成し、メインフォームにTMemoをスローします。メインフォームには、作業の結果とボタンが表示されます。

ボタンハンドラーで、プロキシをテストするためにメインフォームを再作成します。

 procedure TdlgGesturesText.Button1Click(Sender: TObject); begin RecreateWnd; end; 

フォームコンストラクターで、マルチタッチエンジンに接続します。

 procedure TdlgGesturesText.FormCreate(Sender: TObject); var Handlers: TTouchHandlers; begin ZeroMemory(@Handlers, SizeOf(TTouchHandlers)); Handlers.Gesture := OnGesture; TouchManager.RegisterWindow(Self, Handlers); end; 

次に、ハンドラー自体を実装します。

 procedure TdlgGesturesText.OnGesture(Sender: TObject; Control: TWinControl; GestureType: TGestureType; Position: TPoint; Completed: Boolean); begin if not Completed then if not (GestureType in [gt2Left..gt2Down]) then Exit; Memo1.Lines.Add(Format('Control: "%s" gesture "%s" at %dx%d (completed: %s)', [ Control.Name, GetEnumName(TypeInfo(TGestureType), Integer(GestureType)), Position.X, Position.Y, BoolToStr(Completed, True) ])); end; 


ビルド、実行-出来上がり。



ビデオは、サポートされている15個のすべてのジェスチャの認識と、登録されたウィンドウの制御のプロキシを明確に認識します。

実際、これは第10章の終わりで話したのと同じtsimusでした。文字通り数十行のコードで、すべてが箱から出して動作します。

この例のソースコードは、ソースアーカイブの「。\ Demos \ Gestures \フォルダにあります。

14.結論


もちろん、この機能がXE4に存在しないのは残念です。
一方、この瞬間でなければ、そこでどのように機能するのかわからなかったので、プラスもあります。

このアプローチの欠点は、WM_GESTURE + WM_POINTSメッセージの処理が完全に切り取られ、ジェスチャー認識がエンジンのコードに転送されることです。
同意しますが、これは意図的に行われます。

あなた自身がこの方向を掘り下げ始めたら、誰が知っているかもしれませんが、おそらく最終的に私のアプローチに同意するでしょう。少なくとも想像力のフィールドがあります。そのような問題の解決に他にどのようにアプローチできますか。

この記事のデモ例で提供されているCommon.TouchManagerクラスのソースコードは最終的なものではなく、定期的に開発されますが、公に同行するかどうかはわかりません。ただし、ご提案やコメントは大歓迎です。

いつものように、記事を校正してくれたDelphi Mastersフォーラムの参加者に感謝します。

デモのソースコードは、このリンクから入手できます

頑張って

更新:
残念ながら、または幸いなことに、Windows 8以降の一部の機能により、WH_GETMESSAGEトラップはWM_TOUCHメッセージをインターセプトしないため、このコードは機能しません。

このような迷惑を修正するには、トラップの処理を削除し、WM_TOUCHメッセージの処理をプロキシに転送して、次のように書き換える必要があります。
 type //       TWinControlProxy = class(TWinControl) private FOldWndProc: TWndMethod; procedure ParentWndProc(var Message: TMessage); protected procedure DestroyHandle; override; procedure CreateWnd; override; procedure CreateParams(var Params: TCreateParams); override; public destructor Destroy; override; procedure InitParent(Value: TWinControl); end; { TWinControlProxy } // //     WS_EX_TRANSPARENT,    . // ============================================================================= procedure TWinControlProxy.CreateParams(var Params: TCreateParams); begin inherited; Params.ExStyle := Params.ExStyle or WS_EX_TRANSPARENT; end; // //         // ============================================================================= procedure TWinControlProxy.CreateWnd; begin inherited CreateWnd; if Parent.HandleAllocated then RegisterTouchWindow(Parent.Handle, 0); Visible := False; end; // //  ,      // ============================================================================= destructor TWinControlProxy.Destroy; begin if Parent <> nil then Parent.WindowProc := FOldWndProc; inherited; end; // //      ,    // ============================================================================= procedure TWinControlProxy.DestroyHandle; begin if Parent.HandleAllocated then UnregisterTouchWindow(Parent.Handle); Visible := True; inherited DestroyHandle; end; // //   ,     // ============================================================================= procedure TWinControlProxy.InitParent(Value: TWinControl); begin Parent := Value; FOldWndProc := Value.WindowProc; Value.WindowProc := ParentWndProc; end; // //   WM_TOUCH     // ============================================================================= procedure TWinControlProxy.ParentWndProc(var Message: TMessage); var Msg: TMsg; begin if Message.Msg = WM_TOUCH then begin Msg.hwnd := Parent.Handle; Msg.wParam := Message.WParam; Msg.lParam := Message.LParam; TouchManager.HandleMessage(@Msg); end; FOldWndProc(Message); end; 


記事アーカイブでは、これらの変更はすでに行われています。

Source: https://habr.com/ru/post/J242355/


All Articles