Following on from my previous post and remembering that I’m just playing around here and there are better resources up at;
which will lead you to a full, sample interop library wrapper.
I wanted to carry on a little with my experiment though and move up into the world of touch gestures.
This seemed easier as it turns out to be the default and there’s no need to call RegisterTouchWindow if you just want WM_GESTURE messages so that’s nice and I changed my test harness and code behind it to reflect that.
You can also use SetGestureConfig and GetGestureConfig in order to control what gestures you’re interested in for a particular Window and so I wrote a bit of code around that ( again, dropping to C++/CLI to do that ) – not entirely sure that this is correct at this point, mind but here’s the header file;
namespace NativeCodeHelpers { public enum class GestureType { Zoom = 3, Pan = 4, Rotate = 5, TwoFingerTap = 6, PressAndTap = 7 }; } namespace NativeCodeHelpers { [Flags] public enum class PanOptions { All = 1, SingleFingerVertical = 2, SingleFingerHorizontal = 4, WithGutter = 8, WithInertia = 10 }; public ref class GestureConfigOption { public: property GestureType GestureType; property bool IsEnabled; }; public ref class PanGestureConfigOption : GestureConfigOption { public: PanGestureConfigOption(); property PanOptions PanOptions; }; public ref class GestureConfig { public: GestureConfig(IntPtr windowHandle); property List<GestureConfigOption^>^ Configuration { List<GestureConfigOption^>^ get(); }; void AlterConfiguration(List<GestureConfigOption^>^ newConfigItems); private: IntPtr windowHandle; }; }
GestureConfig::GestureConfig(IntPtr windowHandle) { this->windowHandle = windowHandle; } List<GestureConfigOption^>^ GestureConfig::Configuration::get() { // From the docs, these are the supported values although there appear to be // more listed. GESTURECONFIG configs[] = { { GID_ZOOM, 0, 0 }, { GID_ROTATE, 0, 0 }, { GID_PAN, 0, 0 }, { GID_TWOFINGERTAP, 0, 0 }, { GID_PRESSANDTAP, 0, 0 } }; UINT cIds = ARRAYSIZE(configs); List<GestureConfigOption^>^ list = gcnew List<GestureConfigOption^>(); if (::GetGestureConfig((HWND)this->windowHandle.ToPointer(), 0, 0, &cIds, configs, sizeof(GESTURECONFIG))) { for (int i = 0; i < cIds; i++) { GestureConfigOption^ option = nullptr; switch (configs.dwID) { case GID_PAN: { PanGestureConfigOption^ panOption = gcnew PanGestureConfigOption(); panOption->PanOptions = (PanOptions)configs.dwWant; panOption->IsEnabled = (configs.dwWant != 0); option = panOption; } break; default: option = gcnew GestureConfigOption(); option->IsEnabled = (configs.dwBlock == 0) && (configs.dwWant != 0); break; } option->GestureType = (GestureType)configs.dwID; list->Add(option); } } else { int x = GetLastError(); throw gcnew InvalidOperationException(L"Failed getting gesture configuration"); } return(list); } void GestureConfig::AlterConfiguration(List<GestureConfigOption^>^ config) { bool failed = false; DWORD dwAllPanOptions = ( GC_PAN | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY | GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | GC_PAN_WITH_GUTTER | GC_PAN_WITH_INERTIA ); if ((config) && (config->Count > 0)) { GESTURECONFIG* pConfig = new GESTURECONFIG[config->Count]; ZeroMemory(pConfig, sizeof(GESTURECONFIG) * config->Count); for (int i = 0; i < config->Count; i++) { pConfig.dwID = (DWORD)config->GestureType; pConfig.dwWant = (config->IsEnabled) ? 1 : 0; pConfig.dwBlock = (config->IsEnabled) ? 0 : 1; if (config->GestureType == GestureType::Pan) { PanGestureConfigOption^ panOption = (PanGestureConfigOption^)config; pConfig.dwWant = (DWORD)panOption->PanOptions; if (pConfig.dwWant == GC_ALLGESTURES) { pConfig.dwBlock = 0; } else { pConfig.dwBlock = pConfig.dwWant ^ dwAllPanOptions; } } } if (!::SetGestureConfig((HWND)this->windowHandle.ToPointer(), 0, config->Count, pConfig, sizeof(GESTURECONFIG))) { int x = GetLastError(); failed = true; } delete [] pConfig; if (failed) { throw gcnew InvalidOperationException(L"Faield to set gesture configuration"); } } } PanGestureConfigOption::PanGestureConfigOption() { this->GestureType = NativeCodeHelpers::GestureType::Pan; }
config = new GestureConfig(this.Handle); config.AlterConfiguration(new List<GestureConfigOption>() { new GestureConfigOption() { GestureType = GestureType.Zoom, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.Rotate, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.TwoFingerTap, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.PressAndTap, IsEnabled = true }, new PanGestureConfigOption() { PanOptions = PanOptions.All } });
I then needed a bit of helper code to deal with the WM_GESTURE messages for me using the new GetTouchInfo function – again, not entirely sure this C++/CLI is correct but it’s a starting point and here’s the header file;
namespace NativeCodeHelpers { public ref class GestureInfo { public: property GestureType GestureType; property bool IsBegin; property bool IsEnd; property UInt32 X; property UInt32 Y; virtual String^ ToString() override; }; public ref class ZoomGestureInfo : GestureInfo { public: property double FingerDistance; virtual String^ ToString() override; }; public ref class RotateGestureInfo : GestureInfo { public: property double AngleDegrees; virtual String^ ToString() override; }; public ref class GestureDecoder { public: GestureDecoder(IntPtr wParam, IntPtr lParam); bool IsGestureMessage(); property GestureInfo^ Gesture { GestureInfo^ get(); }; private: IntPtr wParam; IntPtr lParam; }; }
followed by the implementation file;
String^ GestureInfo::ToString() { return(String::Format("Gesture {0}, X {1}, Y {2}", this->GestureType, this->X, this->Y)); } String^ RotateGestureInfo::ToString() { return(String::Format("{0}, Angle {1}", GestureInfo::ToString(), this->AngleDegrees)); } String^ ZoomGestureInfo::ToString() { return(String::Format("{0}, Distance {1}", GestureInfo::ToString(), this->FingerDistance)); } GestureDecoder::GestureDecoder(IntPtr wParam, IntPtr lParam) { this->wParam = wParam; this->lParam = lParam; } bool GestureDecoder::IsGestureMessage() { GESTUREINFO gestureInfo; ZeroMemory(&gestureInfo, sizeof(gestureInfo)); gestureInfo.cbSize = sizeof(gestureInfo); BOOL isGestureMessage = ::GetGestureInfo((HGESTUREINFO)lParam.ToPointer(), &gestureInfo); if (isGestureMessage) { // TODO: docs not entirely clear to me as to whether we look for BEGIN/END // in the Flags or in the ID. I'm doing the ID as BEGIN/END on the flags // looks to be useful. isGestureMessage = ((gestureInfo.dwID != GID_BEGIN) && (gestureInfo.dwID != GID_END)); } return((bool)isGestureMessage); } GestureInfo^ GestureDecoder::Gesture::get() { GestureInfo^ info = nullptr; if (!IsGestureMessage()) { throw gcnew InvalidOperationException(L"Not a gesture message"); } else { GESTUREINFO gestureInfo; ZeroMemory(&gestureInfo, sizeof(gestureInfo)); gestureInfo.cbSize = sizeof(gestureInfo); if (::GetGestureInfo((HGESTUREINFO)lParam.ToPointer(), &gestureInfo)) { // TODO: Come back and figure out cbExtraArgs - never seen it set but // then yet to manage to create a two finger tap or a press and tap. switch (gestureInfo.dwID) { case GID_ZOOM: info = gcnew ZoomGestureInfo(); break; case GID_ROTATE: info = gcnew RotateGestureInfo(); break; default: info = gcnew GestureInfo(); break; } info->GestureType = (GestureType)gestureInfo.dwID; info->IsBegin = ((gestureInfo.dwFlags & GF_BEGIN) != 0); info->IsEnd = ((gestureInfo.dwFlags & GF_END) != 0); info->X = gestureInfo.ptsLocation.x; info->Y = gestureInfo.ptsLocation.y; if (info->GestureType == GestureType::Zoom) { ((ZoomGestureInfo^)info)->FingerDistance = gestureInfo.ullArguments; } else if (info->GestureType == GestureType::Rotate) { ((RotateGestureInfo^)info)->AngleDegrees = GID_ROTATE_ANGLE_FROM_ARGUMENT(gestureInfo.ullArguments) * (180 / Math::PI); } // TODO: dangerous to assume true on these. ::CloseGestureInfoHandle((HGESTUREINFO)lParam.ToPointer()); } else { // TODO: Some proper exceptions, perhaps? throw gcnew InvalidOperationException(L"Failed to get gesture info"); } } return(info); }
as an aside, that code comment is wrong – I did figure out how to do a press and tap and a double tap 🙂
Now I can plug that code in to my Windows Forms test harness and see if I can get some WM_TOUCH messages dealt with. I altered my TouchPanel class (C#) in order that it now fires a GestureInputReceived event when it spots a gesture and it uses the previous class GestureDecoder to try and decode what gestures it’s actually seeing.
class TouchInputEventArgs : EventArgs { public List<TouchInput> TouchInput { get; set; } } class GestureInputEventArgs : EventArgs { public GestureInfo GestureInfo { get; set; } } enum TouchType { Touch, Gesture } class TouchPanel : Panel { public event EventHandler<TouchInputEventArgs> TouchInputReceived; public event EventHandler<GestureInputEventArgs> GestureInputReceived; public bool Register(TouchType touchType) { bool returnValue = true; this.touchType = touchType; if (this.touchType == TouchType.Gesture) { InitForGestures(); } else { returnValue = InitForTouch(); } return (returnValue); } void InitForGestures() { config = new GestureConfig(this.Handle); config.AlterConfiguration(new List<GestureConfigOption>() { new GestureConfigOption() { GestureType = GestureType.Zoom, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.Rotate, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.TwoFingerTap, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.PressAndTap, IsEnabled = true }, new PanGestureConfigOption() { PanOptions = PanOptions.All } }); } bool InitForTouch() { return (RegisterTouchWindow(this.Handle, 0)); } public bool Unregister() { bool returnValue = touchType == TouchType.Gesture ? true : UnregisterTouchWindow(this.Handle); return (returnValue); } protected override void DefWndProc(ref Message m) { if ((touchType == TouchType.Touch) && (m.Msg == WM_TOUCH) && (TouchInputReceived != null)) { TouchInputDecoder decoder = new TouchInputDecoder(m.WParam, m.LParam); TouchInputReceived(this, new TouchInputEventArgs() { TouchInput = decoder.Inputs }); } else if ((touchType == TouchType.Gesture) && (m.Msg == WM_GESTURE) && (GestureInputReceived != null)) { GestureDecoder decoder = new GestureDecoder(m.WParam, m.LParam); if (!decoder.IsGestureMessage()) { base.DefWndProc(ref m); } else { GestureInfo info = decoder.Gesture; GestureInputReceived(this, new GestureInputEventArgs() { GestureInfo = info }); } } else { base.DefWndProc(ref m); } } TouchType touchType; GestureConfig config; [DllImport("User32")] private static extern bool RegisterTouchWindow(IntPtr handle, UInt32 flags); [DllImport("User32")] private static extern bool UnregisterTouchWindow(IntPtr handle); // From Winuser.h const int WM_TOUCH = 0x240; const int WM_GESTURE = 0x119; const UInt32 TWF_FINETOUCH = 1; }
and then added this to my Form code which uses this TouchPanel and just picks up the “interesting” events as in;
panel.GestureInputReceived += (s, e) => { if (e.GestureInfo.IsBegin) { AddDebugTextToTextBox("Begun " + e.GestureInfo.ToString()); } else if (e.GestureInfo.IsEnd) { AddDebugTextToTextBox("Ended " + e.GestureInfo.ToString()); } };
and so it only syncs itself up to the Begin/End events to avoid “event over-kill”. With all of that tied together, I can re-run my test form and see some touch events coming through nicely ( note – I’ve only managed to achieve Pan, Rotate, Zoom and I’ve yet to see PressAndTap or TwoFingerTap – not sure how to get those with the CodePlex driver I’m using ). Here’s the app reporting some touch gestures;
Now, it’d be nice if I actually did something with these gestures but manipulating Windows Forms controls isn’t really much fun especially when it comes to trying to do a rotation whereas it’s a lot easier in (say) WPF and so I moved across to WPF and build a little app that referenced the same C++/CLI library but used it to manipulate a rectangle on a WPF canvas ( note – as I mentioned earlier and in the previous post, there are other ways of doing what we’re doing here – I’m just exploring the gesture side of the API rather than presenting a “right way” of doing something ).
I created a little WPF Window which is just a Rectangle on a Canvas;
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="800" Width="600"> <Canvas x:Name="LayoutRoot"> <Canvas.Resources> <Storyboard x:Key="sbTwoFingerTap"> <ColorAnimation Storyboard.TargetName="rectContent" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" From="Yellow" To="Red" Duration="00:00:00.5" AutoReverse="true" FillBehavior="HoldEnd" /> </Storyboard> <Storyboard x:Key="sbPressAndTap"> <ColorAnimation Storyboard.TargetName="rectContent" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" From="Yellow" To="Green" Duration="00:00:00.5" AutoReverse="true" FillBehavior="HoldEnd" /> </Storyboard> </Canvas.Resources> <Rectangle x:Name="rectContent" RadiusX="3" RadiusY="3" Width="192" Height="96" Stroke="Orange" StrokeThickness="2" Canvas.Left="192" Canvas.Top="192" RenderTransformOrigin="0.5,0.5"> <Rectangle.Fill> <SolidColorBrush Color="Yellow" /> </Rectangle.Fill> <Rectangle.RenderTransform> <TransformGroup> <ScaleTransform x:Name="scaleTransform" ScaleX="1" ScaleY="1" /> <RotateTransform x:Name="rotateTransform" Angle="0" /> <TranslateTransform x:Name="translateTransform" /> </TransformGroup> </Rectangle.RenderTransform> </Rectangle> </Canvas> </Window>
and added a little code behind it to use my C++/CLI bits;
public partial class Window1 : Window { public Window1() { InitializeComponent(); this.Loaded += OnLoaded; } void OnLoaded(object sender, RoutedEventArgs e) { sbTwoFingerTap = rectContent.FindResource("sbTwoFingerTap") as Storyboard; sbPressAndTap = rectContent.FindResource("sbPressAndTap") as Storyboard; interopHelper = new WindowInteropHelper(this); GestureConfig config = new GestureConfig(interopHelper.Handle); config.AlterConfiguration(new List<GestureConfigOption>() { new GestureConfigOption() { GestureType = GestureType.Zoom, IsEnabled = true }, new PanGestureConfigOption() { IsEnabled = true, PanOptions = PanOptions.All }, new GestureConfigOption() { GestureType = GestureType.PressAndTap, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.TwoFingerTap, IsEnabled = true }, new GestureConfigOption() { GestureType = GestureType.Rotate, IsEnabled = true } }); HwndSource source = HwndSource.FromHwnd(interopHelper.Handle); HwndSourceHook hook = new HwndSourceHook(WndProc); source.AddHook(hook); } static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { handled = false; GestureDecoder decoder = new GestureDecoder(wParam, lParam); if (decoder.IsGestureMessage()) { handled = true; GestureInfo gi = decoder.Gesture; Window1 window = (Window1)App.Current.MainWindow; window.HandleGesture(gi); } return (IntPtr.Zero); } void HandleGesture(GestureInfo gi) { switch (gi.GestureType) { case GestureType.Pan: HandlePan(gi); break; case GestureType.PressAndTap: sbPressAndTap.Stop(); sbPressAndTap.Begin(); break; case GestureType.Rotate: HandleRotation(gi); break; case GestureType.TwoFingerTap: sbTwoFingerTap.Stop(); sbTwoFingerTap.Begin(); break; case GestureType.Zoom: HandleZoom(gi); break; default: break; } } void HandlePan(GestureInfo gi) { if (gi.IsBegin) { lastPoint = new Point(gi.X, gi.Y); } else { Point curPoint = new Point(gi.X, gi.Y); double xDelta = curPoint.X - lastPoint.X; double yDelta = curPoint.Y - lastPoint.Y; translateTransform.X += xDelta; translateTransform.Y += yDelta; lastPoint = curPoint; } } void HandleZoom(GestureInfo gi) { ZoomGestureInfo zi = (ZoomGestureInfo)gi; if (zi.IsBegin) { firstZoom = zi.FingerDistance; } else { double delta = zi.FingerDistance - firstZoom; firstZoom = zi.FingerDistance; Debug.WriteLine(string.Format("{0}, {1}", DateTime.Now.Ticks, delta)); double scaleX = (delta / scaleSensitivityMagicFactor); double scaleY = (delta / scaleSensitivityMagicFactor); scaleTransform.ScaleX += scaleX; scaleTransform.ScaleY += scaleY; } } void HandleRotation(GestureInfo gi) { RotateGestureInfo ri = (RotateGestureInfo)gi; if (ri.IsBegin) { captureAngle = true; } else { if (captureAngle) { lastAngle = ri.AngleDegrees; captureAngle = false; } else { double delta = ri.AngleDegrees - lastAngle; Debug.WriteLine(string.Format("{0}, Delta is {1}", DateTime.Now.Ticks, delta)); rotateTransform.Angle += 0 - (delta / rotateSensitivityMagicFactor); if (ri.IsEnd) { captureAngle = true; lastAngle = 0; } } } } bool captureAngle; double lastAngle; double firstZoom; Point lastPoint; const double scaleSensitivityMagicFactor = 200.0; const double rotateSensitivityMagicFactor = 25.0; WindowInteropHelper interopHelper; Storyboard sbTwoFingerTap; Storyboard sbPressAndTap; }
and that all seemed to work out reasonably well ( notice the magic factors around lines 145/146 ) in that I’ve got an app that’ll respond to the 5 gestures that I’m asking it to;
Now, it turns out that using touch to physically manipulate an object like this is a common enough thing to want to do that there’s already a whole API in Windows 7 to handle it – the manipulation API.
As it happens, that API is built as an unmanaged COM API and it’s already wrapped up by the sample interop code up here ( as referenced earlier );
and so I think I’ll switch from using my own wrappers now to using those libraries rather than re-inventing that particular wheel.
That’ll be my next post…but in the meantime the source code for where I’ve got to so far is for download from here if you want to just have a play around with it.