Multi-Touch APIをWPFで利用する方法②
2015年06月08日
前回に引き続き、Wacom feel™ Multi-Touch APIをWPFで利用する方法を紹介します。
前回は、タッチデバイスの情報を取得するところまで行いましたが、実際にタッチイベントを取得する方法を紹介します。
タッチイベントを受信する方法は、以下の二種類が存在します。
・コールバック
・ウィンドウメッセージ
また、OSへのタッチイベント通知を有効・無効にするためのモード設定もあります。
今回は、タッチイベントを受信する方法とモードをコンボボックスで切り替えられるような機能を実装します。
1. Wacom feel™ Multi-Touch APIの関数定義追加
Multi-Touch API用の関数定義用の「MTAPI.cs」へタッチイベントを受信するために必要な関数を追加します。
また、タッチイベントの受信設定を解除するための関数も追加しておきます。
// フィンガーデータを受信するウィンドウハンドルを設定 [DllImport("wacommt.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WacomMTRegisterFingerReadHWND")] public static extern WacomMTError WacomMTRegisterFingerReadHWND(int deviceID, WacomMTProcessingMode mode, IntPtr hWnd, int bufferDepth); /// フィンガーデータを受信するウィンドウハンドルを解除 [DllImport("wacommt.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WacomMTUnRegisterFingerReadHWND")] public static extern WacomMTError WacomMTUnRegisterFingerReadHWND(IntPtr hwnd); /// フィンガーデータを受け取るコールバックの設定 [DllImport("wacommt.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WacomMTRegisterFingerReadCallback")] public static extern WacomMTError WacomMTRegisterFingerReadCallback(int deviceID, IntPtr hitRect, WacomMTProcessingMode mode, [MarshalAs(UnmanagedType.FunctionPtr)] WMT_FINGER_CALLBACK fingerCallback, IntPtr userData); /// フィンガーデータを受け取るコールバックの解除 [DllImport("wacommt.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WacomMTUnRegisterFingerReadCallback")] public static extern WacomMTError WacomMTUnRegisterFingerReadCallback(int deviceID, IntPtr hitRect, WacomMTProcessingMode mode, IntPtr userData);
上記以外にも「FeelMultiTouchAPIConst.cs」へ必要な構造体やdelegateを追加しています。
2.イベント・モードの種類変更用のComboBox設置
「MainWindow.xaml」へイベント・モードを切り替えるComboBoxを設置します。
選択変更のイベントは全てメソッドを指定します。
<Window x:Class="WacomFeelMultiTouchAPI_WPF.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="23"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Grid.Row="0"> <ComboBox Name="devicesBox" Height="23" Width="200" SelectionChanged="ComboBox_SelectionChanged"/> <ComboBox Name="eventTypeBox" Height="23" Width="150" SelectedIndex="0" SelectionChanged="ComboBox_SelectionChanged"> <ComboBoxItem Content="Callback" /> <ComboBoxItem Content="WindowMessage" /> </ComboBox> <ComboBox Name="modeBox" Height="23" Width="150" SelectedIndex="0" SelectionChanged="ComboBox_SelectionChanged"> <ComboBoxItem Content="None" /> <ComboBoxItem Content="Observer" /> </ComboBox> </StackPanel> <Canvas Name="fingerCanvas" Grid.Row="1" Background="#EEEEEE"/> <TextBlock Name="capabilitiesText" Background="Transparent" Grid.Row="1" VerticalAlignment="Top" HorizontalAlignment="Left" Foreground="Black" TextWrapping="Wrap"/> </Grid> </Window>
ComboBox_SelectionChangedは、「MainWindow.cs」へ以下のように追加します。
RegisterEventはComboBoxの選択状態を見てタッチイベントを登録するメソッドです。
次の項目でメソッドを追加します。
// コンボボックスの変更イベント private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { RegisterEvent(); }
3.タッチイベントの登録・解除処理の追加
タッチイベントの登録・解除処理を「MainWindow.cs」に追加します。
/// タッチイベントの解除 void UnregisterEvent() { // 画面上の円を削除 fingerCanvas.Children.Clear(); if (mSelectedHwnd != IntPtr.Zero) { MTAPI.WacomMTUnRegisterFingerReadHWND(mSelectedHwnd); mSelectedHwnd = IntPtr.Zero; } if (mFingerCallback != null) { MTAPI.WacomMTUnRegisterFingerReadCallback(mSelectedDeviceID, mHitRectPtr, mCurrentMode, IntPtr.Zero); mFingerCallback = null; } // HitRectの開放 if (mHitRectPtr != IntPtr.Zero) { MemoryUtil.FreeUnmanagedBuffer(mHitRectPtr); } mSelectedDeviceID = -1; } /// タッチイベントの取得方法設定 void RegisterEvent() { if (capabilitiesText == null) { return; } // タッチデバイスのイベント解除 UnregisterEvent(); capabilitiesText.Text = ""; if (devicesBox.SelectedIndex != 0) { try { // タブレットの情報を取得し、画面上に表示する mCapability = MTAPI.WacomMTGetDeviceCapabilities(mIdArray[devicesBox.SelectedIndex - 1]); String t = ""; t += "Version : " + mCapability.Version + "\n"; t += "DeviceID : " + mCapability.DeviceID + "\n"; t += "Type : " + mCapability.Type + "\n"; t += "LogicalOriginX : " + mCapability.LogicalOriginX + "\n"; t += "LogicalOriginY : " + mCapability.LogicalOriginY + "\n"; t += "LogicalWidth : " + mCapability.LogicalWidth + "\n"; t += "LogicalHeight : " + mCapability.LogicalHeight + "\n"; t += "PhysicalSizeX : " + mCapability.PhysicalSizeX + "\n"; t += "PhysicalSizeY : " + mCapability.PhysicalSizeY + "\n"; t += "ReportedSizeX : " + mCapability.ReportedSizeX + "\n"; t += "ReportedSizeY : " + mCapability.ReportedSizeY + "\n"; t += "ScanSizeX : " + mCapability.ScanSizeX + "\n"; t += "ScanSizeY : " + mCapability.ScanSizeY + "\n"; t += "FingerMax : " + mCapability.FingerMax + "\n"; t += "BlobMax : " + mCapability.BlobMax + "\n"; t += "BlobPointsMax : " + mCapability.BlobPointsMax + "\n"; t += "CapabilityFlags : " + mCapability.CapabilityFlags + "\n"; capabilitiesText.Text = t; // 選択中のタッチデバイスのID mSelectedDeviceID = mCapability.DeviceID; // モードの選択 if (modeBox.SelectedIndex == 0) { // モードをWMTProcessingModeNoneに設定 mCurrentMode = WacomMTProcessingMode.WMTProcessingModeNone; } else { // モードをWMTProcessingModeObserverに設定 mCurrentMode = WacomMTProcessingMode.WMTProcessingModeObserver; } // イベントのタイプを選択 if (eventTypeBox.SelectedIndex == 0) { // タッチエリアの設定 WacomMTHitRect hr; hr.originX = 0; hr.originY = 0; hr.width = (float)fingerCanvas.ActualWidth; hr.height = (float)fingerCanvas.ActualHeight; mHitRectPtr = MemoryUtil.AllocUnmanagedBuffer(hr); // コールバックの設定 mFingerCallback = new WMT_FINGER_CALLBACK(wmtFingerCallback); MTAPI.WacomMTRegisterFingerReadCallback(mCapability.DeviceID, mHitRectPtr, mCurrentMode, mFingerCallback, IntPtr.Zero); } else { // ウインドウメッセージ mSelectedHwnd = mHwnd; MTAPI.WacomMTRegisterFingerReadHWND(mCapability.DeviceID, mCurrentMode, mHwnd, 5); } } catch (Exception ex) { MessageBox.Show(ex.Message); } } }
RegisterEvent内でComboBoxで選択しているモードやイベントで対象のタッチデバイスからタッチイベントを取得するよう設定しています。
モードはWacomMTProcessingModeで指定しており、二種類を設定することが可能です。
・WMTProcessingModeNone
アプリのみにタッチイベントを通知し、OS側にはタッチイベントを通知しません。
・WMTProcessingModeObserver
アプリ・OS側両方にタッチイベントを通知します。
タッチイベントの受信方法は冒頭に記載したとおり、コールバックとウィンドウメッセージの二種類が存在します。
コールバックは「WacomMTRegisterFingerReadCallback」、ウィンドウメッセージは「WacomMTRegisterFingerReadHWND」で設定を行っています。
4.タッチの描画処理の実装
タッチの描画処理を「MainWindow.xaml」へ追加します。
/// フィンガーデータの配列から描画を行う /// <param name="fingers"></param> void DrawFingers(WacomMTFinger[] fingers) { // 画面上の円を削除 fingerCanvas.Children.Clear(); // 指のデータの円を表示 foreach (WacomMTFinger fig in fingers) { Ellipse ellipse = new Ellipse(); // 円の色設定 ellipse.Fill = Brushes.Black; // 円のサイズ設定 if (fig.Width <= 1.0f) { // 1.0以下の場合はタブレットのサイズに対する割合 ellipse.Width = fig.Width * mCapability.PhysicalSizeX; } else { // 1.0より大きい場合はピクセル(24HD) ellipse.Width = fig.Width; } if (fig.Height <= 1.0f) { // 1.0以下の場合はタブレットのサイズに対する割合 ellipse.Height = fig.Height * mCapability.PhysicalSizeY; } else { // 1.0より大きい場合はピクセル(24HD) ellipse.Height = fig.Height; } // 指の位置をスクリーンの座標に変換 Point po = new Point(); if (fig.X <= 1.0f) { po.X = fig.X * fingerCanvas.ActualWidth; } else { po.X = fig.X; } if (fig.Y <= 1.0f) { po.Y = fig.Y * fingerCanvas.ActualHeight; } else { po.Y = fig.Y; } // 座標は中央の座標なので左上に移動する Canvas.SetLeft(ellipse, po.X - ellipse.Width / 2); Canvas.SetTop(ellipse, po.Y - ellipse.Height / 2); // Canvasへ追加 fingerCanvas.Children.Add(ellipse); } } /// <summary> /// フィンガーデータのIntPtrから描画を行う /// </summary> /// <param name="pFingers"></param> void DrawFingers(IntPtr pFingers) { // タッチデータの受信 WacomMTFingerCollection collection = (WacomMTFingerCollection)MemoryUtil.MarshalUnmanagedBuffer(pFingers, typeof(WacomMTFingerCollection)); WacomMTFinger[] fingers = MemoryUtil.MarshalUnmanagedBufferArray<WacomMTFinger>(collection.FingerData, (uint)collection.FingerCount); DrawFingers(fingers); }
フィンガーデータの座標やサイズが1.0以下かどうかチェックを行い、円の座標やサイズを設定していますが、これはタッチデバイスによってデータのフォーマットが違うためです。
一般的なタッチデバイスでは0.0~1.0の割合で座標やサイズが指定されていますが、Cintiq 24HDでは座標が渡されます。
また、座標は指の中央の座標ですので描画時にずらして表示しています。
5.ウィンドウプロシージャ・コールバックの実装
実際にタッチデータを受信するウィンドウプロシージャとコールバックを「MainWindow.cs」へ実装します。
// ウインドウのロードイベント void MainWindow_Loaded(object sender, RoutedEventArgs e) { // ウインドウハンドルの取得 mHwnd = new WindowInteropHelper(this).Handle; HwndSource source = HwndSource.FromHwnd(mHwnd); source.AddHook(new HwndSourceHook(WndProc)); } // ウインドウプロシージャ private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WindowMessage.WM_FINGERDATA) { DrawFingers(lParam); } return IntPtr.Zero; } /// フィンガーデータのコールバック private int wmtFingerCallback(IntPtr fingerPacket, IntPtr userData) { // コールバック内でIntPtrから構造体に変換しないと、IntPtrが開放されてしまう WacomMTFingerCollection collection = (WacomMTFingerCollection)MemoryUtil.MarshalUnmanagedBuffer(fingerPacket, typeof(WacomMTFingerCollection)); WacomMTFinger[] fingers = MemoryUtil.MarshalUnmanagedBufferArray<WacomMTFinger>(collection.FingerData, (uint)collection.FingerCount); // コールバック内ではコントロールを変更できないのでInvokeを利用する fingerCanvas.Dispatcher.BeginInvoke(new Action(() => { DrawFingers(fingers); }), null); return 0; }
ウィンドウプロシージャはlParam、コールバックはfingerPacketがタッチデータとなっています。
データの構造としては同じWacomMTFingerCollectionなので描画処理の内容は同じDrawFingersを利用しますが、若干処理の流れが違います。
ウィンドウプロシージャは特に問題はないですが、コールバックの場合に注意が必要となります。
コールバック内ではコントロールを直接編集することができないので、Invokeを利用してコントロールを編集していますが、Invoke内でfingerPacketを構造体に変換しようとしてもすでにメモリが開放されているためタッチデータを取得することができません。
上記のソースコードのようにfingerPacketはコールバック内で構造体に変換し、Invokeで構造体を処理すると正常にタッチイベントを処理することができるようになります。
以上で実装は完了したので、実際にアプリケーションを動作させてみましょう。
タッチデバイスを選択し、タッチするとアプリケーション上に黒い円が表示されるかと思います。
サンプルソースはこちらにアップしてありますので、興味のある方は実際に試して頂ければと思います。
次回は、フィンガーデータ以外のフォーマットのタッチイベントについて紹介します。