diff --git a/src/Common.WinForms/Controls/GestureEventArgs.cs b/src/Common.WinForms/Controls/GestureEventArgs.cs new file mode 100644 index 000000000..af7f49983 --- /dev/null +++ b/src/Common.WinForms/Controls/GestureEventArgs.cs @@ -0,0 +1,100 @@ +// Copyright Bastian Eicher +// Licensed under the MIT License + +using System; + +namespace NanoByte.Common.Controls; + +/// +/// Flags for gesture information. +/// +[Flags] +public enum GestureFlags +{ + /// Marks the beginning of a gesture. + Begin = 0x00000001, + + /// Indicates that the gesture is in inertia mode. + Inertia = 0x00000002, + + /// Marks the end of a gesture. + End = 0x00000004 +} + +/// +/// Base class for gesture event arguments. +/// +public abstract class GestureEventArgs : EventArgs +{ + /// + /// The X coordinate of the gesture in client coordinates. + /// + public int LocationX { get; set; } + + /// + /// The Y coordinate of the gesture in client coordinates. + /// + public int LocationY { get; set; } + + /// + /// Flags indicating the state of the gesture. + /// + public GestureFlags Flags { get; set; } + + /// + /// Unique identifier for this gesture sequence. + /// + public int SequenceId { get; set; } +} + +/// +/// Event arguments for pan gesture. +/// +public class PanGestureEventArgs : GestureEventArgs +{ + /// + /// The horizontal distance panned. + /// + public int PanDistanceX { get; set; } + + /// + /// The vertical distance panned. + /// + public int PanDistanceY { get; set; } +} + +/// +/// Event arguments for zoom gesture. +/// +public class ZoomGestureEventArgs : GestureEventArgs +{ + /// + /// The distance between the two fingers. + /// + public long Distance { get; set; } +} + +/// +/// Event arguments for rotate gesture. +/// +public class RotateGestureEventArgs : GestureEventArgs +{ + /// + /// The angle of rotation in radians. + /// + public double Angle { get; set; } +} + +/// +/// Event arguments for tap gesture (two-finger tap). +/// +public class TapGestureEventArgs : GestureEventArgs +{ +} + +/// +/// Event arguments for press and tap gesture. +/// +public class PressAndTapGestureEventArgs : GestureEventArgs +{ +} diff --git a/src/Common.WinForms/Controls/ITouchControl.cs b/src/Common.WinForms/Controls/ITouchControl.cs index 34f462f6f..cbbe45e8c 100644 --- a/src/Common.WinForms/Controls/ITouchControl.cs +++ b/src/Common.WinForms/Controls/ITouchControl.cs @@ -4,22 +4,32 @@ namespace NanoByte.Common.Controls; /// -/// A control that can raise touch events. +/// A control that can raise touch gesture events. /// public interface ITouchControl { /// - /// Raised when the user begins touching the screen. + /// Raised when the user performs a pan gesture. /// - event EventHandler TouchDown; + event EventHandler Pan; /// - /// Raised when the user stops touching the screen. + /// Raised when the user performs a zoom gesture. /// - event EventHandler TouchUp; + event EventHandler Zoom; /// - /// Raised when the user moves fingers while touching the screen. + /// Raised when the user performs a rotate gesture. /// - event EventHandler TouchMove; + event EventHandler Rotate; + + /// + /// Raised when the user performs a two-finger tap gesture. + /// + event EventHandler Tap; + + /// + /// Raised when the user performs a press and tap gesture. + /// + event EventHandler PressAndTap; } diff --git a/src/Common.WinForms/Controls/TouchEventArgs.cs b/src/Common.WinForms/Controls/TouchEventArgs.cs deleted file mode 100644 index de5a5eb9b..000000000 --- a/src/Common.WinForms/Controls/TouchEventArgs.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright Bastian Eicher -// Licensed under the MIT License - -namespace NanoByte.Common.Controls; - -// ReSharper disable CommentTypo -/// -/// Mask indicating which fields in are valid. -/// -/// -[SuppressMessage("Microsoft.Naming", "CA1714:FlagsEnumsShouldHavePluralNames", Justification = "The keyword mask implies flag usage without the need for a plural form")] -[Flags] -public enum TouchEventMask -{ - /// TOUCHINPUTMASKF_TIMEFROMSYSTEM - Time = 0x0001, - - /// TOUCHINPUTMASKF_EXTRAINFO - ExtraInfo = 0x0002, - - /// TOUCHINPUTMASKF_CONTACTAREA - ContactArea = 0x0004 -} - -/// -/// Event information about a touch event. -/// -public class TouchEventArgs : EventArgs -{ - /// - /// Touch X client coordinate in pixels. - /// - public int LocationX { get; set; } - - /// - /// Touch Y client coordinate in pixels. - /// - public int LocationY { get; set; } - - /// - /// X size of the contact area in pixels. - /// - public int ContactX { get; set; } - - /// - /// X size of the contact area in pixels. - /// - public int ContactY { get; set; } - - /// - /// Contact ID. - /// - public int ID { get; set; } - - /// - /// Mask indicating which fields in the structure are valid. - /// - public TouchEventMask Mask { get; set; } - - /// - /// Touch event time. - /// - public int Time { get; set; } - - /// - /// Indicates that this structure corresponds to a primary contact point. - /// - public bool Primary; - - /// - /// The touch event came from the user's palm. - /// - public bool Palm; - - /// - /// The user is hovering above the touch screen. - /// - public bool InRange; - - /// - /// This input was not coalesced. - /// - public bool NoCoalesce; -} diff --git a/src/Common.WinForms/Controls/TouchForm.cs b/src/Common.WinForms/Controls/TouchForm.cs index 0ed4edec2..759041028 100644 --- a/src/Common.WinForms/Controls/TouchForm.cs +++ b/src/Common.WinForms/Controls/TouchForm.cs @@ -10,18 +10,24 @@ namespace NanoByte.Common.Controls; /// -/// Represents a window that reacts to touch input on Windows 7 or newer. +/// Represents a window that reacts to touch gestures on Windows 7 or newer. /// public class TouchForm : Form, ITouchControl { /// - public event EventHandler? TouchDown; + public event EventHandler? Pan; /// - public event EventHandler? TouchUp; + public event EventHandler? Zoom; /// - public event EventHandler? TouchMove; + public event EventHandler? Rotate; + + /// + public event EventHandler? Tap; + + /// + public event EventHandler? PressAndTap; protected override CreateParams CreateParams { @@ -39,7 +45,7 @@ protected override CreateParams CreateParams protected override void CreateHandle() { base.CreateHandle(); - WinFormsUtils.RegisterTouchWindow(this); + WinFormsUtils.RegisterGestureWindow(this); } #if NETFRAMEWORK @@ -47,7 +53,9 @@ protected override void CreateHandle() #endif protected override void WndProc(ref Message m) { - WinFormsUtils.HandleTouchMessage(ref m, this, TouchDown, TouchMove, TouchUp); + bool handled = WinFormsUtils.HandleGestureMessage(ref m, this, Pan, Zoom, Rotate, Tap, PressAndTap); base.WndProc(ref m); + if (handled) + m.Result = new IntPtr(1); } } diff --git a/src/Common.WinForms/Controls/TouchPanel.cs b/src/Common.WinForms/Controls/TouchPanel.cs index dbdbf93a2..28d812070 100644 --- a/src/Common.WinForms/Controls/TouchPanel.cs +++ b/src/Common.WinForms/Controls/TouchPanel.cs @@ -10,18 +10,24 @@ namespace NanoByte.Common.Controls; /// -/// Represents a panel that reacts to touch input on Windows 7 or newer. +/// Represents a panel that reacts to touch gestures on Windows 7 or newer. /// public class TouchPanel : Panel, ITouchControl { /// - public event EventHandler? TouchDown; + public event EventHandler? Pan; /// - public event EventHandler? TouchUp; + public event EventHandler? Zoom; /// - public event EventHandler? TouchMove; + public event EventHandler? Rotate; + + /// + public event EventHandler? Tap; + + /// + public event EventHandler? PressAndTap; protected override CreateParams CreateParams { @@ -39,7 +45,7 @@ protected override CreateParams CreateParams protected override void CreateHandle() { base.CreateHandle(); - WinFormsUtils.RegisterTouchWindow(this); + WinFormsUtils.RegisterGestureWindow(this); } #if NETFRAMEWORK @@ -47,7 +53,9 @@ protected override void CreateHandle() #endif protected override void WndProc(ref Message m) { - WinFormsUtils.HandleTouchMessage(ref m, this, TouchDown, TouchMove, TouchUp); + bool handled = WinFormsUtils.HandleGestureMessage(ref m, this, Pan, Zoom, Rotate, Tap, PressAndTap); base.WndProc(ref m); + if (handled) + m.Result = new IntPtr(1); } } diff --git a/src/Common.WinForms/Native/WinFormsUtils.NativeMethods.cs b/src/Common.WinForms/Native/WinFormsUtils.NativeMethods.cs index 244c9a4c8..e1dc20c3f 100644 --- a/src/Common.WinForms/Native/WinFormsUtils.NativeMethods.cs +++ b/src/Common.WinForms/Native/WinFormsUtils.NativeMethods.cs @@ -55,50 +55,65 @@ public struct WinMessage [return: MarshalAs(UnmanagedType.Bool)] public static extern bool ReleaseCapture(); - [DllImport("user32")] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool RegisterTouchWindow(IntPtr hWnd, uint ulFlags); - - [DllImport("user32")] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GetTouchInputInfo(IntPtr hTouchInput, int cInputs, [In, Out] TouchInput[] pInputs, int cbSize); + // Gesture-related constants + public const int GestureConfigAll = 0x00000001; // GC_ALLGESTURES + + // Gesture IDs + public const int GestureIdBegin = 1; + public const int GestureIdEnd = 2; + public const int GestureIdZoom = 3; + public const int GestureIdPan = 4; + public const int GestureIdRotate = 5; + public const int GestureIdTwoFingerTap = 6; + public const int GestureIdPressAndTap = 7; + + // Gesture flags + public const int GestureFlagBegin = 0x00000001; + public const int GestureFlagInertia = 0x00000002; + public const int GestureFlagEnd = 0x00000004; - [Flags] - public enum TouchEvents + [StructLayout(LayoutKind.Sequential)] + [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")] + [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")] + public struct GestureConfig { - Move = 0x0001, // TOUCHEVENTF_MOVE - Down = 0x0002, // TOUCHEVENTF_DOWN - Up = 0x0004, // TOUCHEVENTF_UP - InRange = 0x0008, // TOUCHEVENTF_INRANGE - Primary = 0x0010, // TOUCHEVENTF_PRIMARY - NoCoalesce = 0x0020, // TOUCHEVENTF_NOCOALESCE - Palm = 0x0080 // TOUCHEVENTF_PALM + public int dwID; // gesture ID + public int dwWant; // settings related to gesture ID that are to be turned on + public int dwBlock; // settings related to gesture ID that are to be turned off } [StructLayout(LayoutKind.Sequential)] [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")] - public struct TouchInput + [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")] + public struct Points { - public int x, y; - public IntPtr hSource; - public int dwID; - public TouchEvents dwFlags; - public TouchEventMask dwMask; - public int dwTime; - public IntPtr dwExtraInfo; - public int cxContact; - public int cyContact; + public short x; + public short y; } [StructLayout(LayoutKind.Sequential)] [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")] - private struct Points + [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")] + public struct GestureInfo { - public short x, y; + public int cbSize; // size, in bytes, of this structure + public int dwFlags; // see GF_* flags + public int dwID; // gesture ID, see GID_* defines + public IntPtr hwndTarget; // handle to window targeted by this gesture + [MarshalAs(UnmanagedType.Struct)] + public Points ptsLocation; // current location of this gesture + public int dwInstanceID; // internally used + public int dwSequenceID; // internally used + public long ullArguments; // arguments for gestures whose arguments fit in 8 BYTES + public int cbExtraArgs; // size, in bytes, of extra arguments, if any, that accompany this gesture } [DllImport("user32")] [return: MarshalAs(UnmanagedType.Bool)] - public static extern void CloseTouchInputHandle(IntPtr lParam); + public static extern bool SetGestureConfig(IntPtr hWnd, int dwReserved, int cIDs, ref GestureConfig pGestureConfig, int cbSize); + + [DllImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetGestureInfo(IntPtr hGestureInfo, ref GestureInfo pGestureInfo); } } diff --git a/src/Common.WinForms/Native/WinFormsUtils.Touch.cs b/src/Common.WinForms/Native/WinFormsUtils.Touch.cs index 7d2dd21f8..f6a20ceb0 100644 --- a/src/Common.WinForms/Native/WinFormsUtils.Touch.cs +++ b/src/Common.WinForms/Native/WinFormsUtils.Touch.cs @@ -7,94 +7,186 @@ namespace NanoByte.Common.Native; partial class WinFormsUtils { - // Note: The following code is based on Windows API Code Pack for Microsoft .NET Framework 1.0.1 + // Note: The following code is based on Windows 7 Touch Gesture API + // See: https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/Touch/MTGestures/CS/MTGestures.cs + + private static readonly int _gestureConfigSize = Marshal.SizeOf(typeof(NativeMethods.GestureConfig)); /// - /// Registers a control as a receiver for touch events. + /// Registers a control to receive gesture events. + /// This method is kept for API compatibility but gesture configuration is done automatically. /// /// The control to register. - public static void RegisterTouchWindow(Control control) + public static void RegisterGestureWindow(Control control) { #region Sanity checks if (control == null) throw new ArgumentNullException(nameof(control)); #endregion - if (WindowsUtils.IsWindows7) NativeMethods.RegisterTouchWindow(control.Handle, 0); + // Gesture configuration is done in response to WM_GESTURENOTIFY + // No registration call is needed upfront } - private static readonly int _touchInputSize = Marshal.SizeOf(new NativeMethods.TouchInput()); + /// + /// Configures which gestures are enabled for a window. + /// + /// Handle to the window. + public static void ConfigureGestures(IntPtr hWnd) + { + if (!WindowsUtils.IsWindows7) return; + + var gc = new NativeMethods.GestureConfig + { + dwID = 0, // gesture ID + dwWant = NativeMethods.GestureConfigAll, // enable all gestures + dwBlock = 0 // block no gestures + }; + + NativeMethods.SetGestureConfig( + hWnd, + 0, + 1, + ref gc, + _gestureConfigSize); + } /// - /// Handles touch-related s. + /// Handles gesture-related s. /// /// The message to handle. /// The object to send possible events from. - /// The event handler to call for touch down events; can be null. - /// The event handler to call for touch move events; can be null. - /// The event handler to call for touch up events; can be null. + /// The event handler to call for pan gestures; can be null. + /// The event handler to call for zoom gestures; can be null. + /// The event handler to call for rotate gestures; can be null. + /// The event handler to call for two-finger tap gestures; can be null. + /// The event handler to call for press and tap gestures; can be null. + /// true if the message was handled; otherwise false. [SuppressMessage("ReSharper", "InconsistentNaming")] - public static void HandleTouchMessage(ref Message m, object? sender, EventHandler? onTouchDown, EventHandler? onTouchMove, EventHandler? onTouchUp) + public static bool HandleGestureMessage( + ref Message m, + object? sender, + EventHandler? onPan, + EventHandler? onZoom, + EventHandler? onRotate, + EventHandler? onTap, + EventHandler? onPressAndTap) { - const int WM_TOUCHMOVE = 0x0240, WM_TOUCHDOWN = 0x0241, WM_TOUCHUP = 0x0242; + const int WM_GESTURENOTIFY = 0x011A; + const int WM_GESTURE = 0x0119; - if (!WindowsUtils.IsWindows7) return; - if (m.Msg != WM_TOUCHDOWN && m.Msg != WM_TOUCHMOVE && m.Msg != WM_TOUCHUP) return; - - // More than one touchinput may be associated with a touch message, - // so an array is needed to get all event information. - short inputCount = (short)(m.WParam.ToInt32() & 0xffff); // Number of touch inputs, actual per-contact messages - - if (inputCount < 0) return; - var inputs = new NativeMethods.TouchInput[inputCount]; - - // Unpack message parameters into the array of TOUCHINPUT structures, each - // representing a message for one single contact. - //Exercise2-Task1-Step3 - if (!NativeMethods.GetTouchInputInfo(m.LParam, inputCount, inputs, _touchInputSize)) - return; - - // For each contact, dispatch the message to the appropriate message - // handler. - // Note that for WM_TOUCHDOWN you can get down & move notifications - // and for WM_TOUCHUP you can get up & move notifications - // WM_TOUCHMOVE will only contain move notifications - // and up & down notifications will never come in the same message - for (int i = 0; i < inputCount; i++) + if (!WindowsUtils.IsWindows7) return false; + + if (m.Msg == WM_GESTURENOTIFY) + { + ConfigureGestures(m.HWnd); + return true; + } + + if (m.Msg != WM_GESTURE) return false; + + var gi = new NativeMethods.GestureInfo { cbSize = Marshal.SizeOf(typeof(NativeMethods.GestureInfo)) }; + + if (!NativeMethods.GetGestureInfo(m.LParam, ref gi)) + return false; + + // Convert screen coordinates to client coordinates + var location = Control.FromHandle(m.HWnd)?.PointToClient(new Point(gi.ptsLocation.x, gi.ptsLocation.y)) ?? new Point(gi.ptsLocation.x, gi.ptsLocation.y); + + var flags = (GestureFlags)gi.dwFlags; + + switch (gi.dwID) { - NativeMethods.TouchInput ti = inputs[i]; - - // Assign a handler to this message. - EventHandler? handler = null; // Touch event handler - if (ti.dwFlags.HasFlag(NativeMethods.TouchEvents.Down)) handler = onTouchDown; - else if (ti.dwFlags.HasFlag(NativeMethods.TouchEvents.Up)) handler = onTouchUp; - else if (ti.dwFlags.HasFlag(NativeMethods.TouchEvents.Move)) handler = onTouchMove; - - // Convert message parameters into touch event arguments and handle the event. - if (handler != null) - { - // TOUCHINFO point coordinates and contact size is in 1/100 of a pixel; convert it to pixels. - // Also convert screen to client coordinates. - var te = new TouchEventArgs + case NativeMethods.GestureIdBegin: + case NativeMethods.GestureIdEnd: + // These are informational only + break; + + case NativeMethods.GestureIdPan: + if (onPan != null) + { + // ullArguments contains the total pan distance as two 32-bit signed integers: + // Lower 32 bits = X distance (horizontal pan) + // Upper 32 bits = Y distance (vertical pan) + var args = new PanGestureEventArgs + { + LocationX = location.X, + LocationY = location.Y, + Flags = flags, + SequenceId = gi.dwSequenceID, + PanDistanceX = (int)(gi.ullArguments & 0xFFFFFFFF), + PanDistanceY = (int)((gi.ullArguments >> 32) & 0xFFFFFFFF) + }; + onPan(sender, args); + } + break; + + case NativeMethods.GestureIdZoom: + if (onZoom != null) + { + var args = new ZoomGestureEventArgs + { + LocationX = location.X, + LocationY = location.Y, + Flags = flags, + SequenceId = gi.dwSequenceID, + Distance = gi.ullArguments + }; + onZoom(sender, args); + } + break; + + case NativeMethods.GestureIdRotate: + if (onRotate != null) { - ContactY = ti.cyContact / 100, - ContactX = ti.cxContact / 100, - ID = ti.dwID, - LocationX = ti.x / 100, - LocationY = ti.y / 100, - Time = ti.dwTime, - Mask = ti.dwMask, - InRange = ti.dwFlags.HasFlag(NativeMethods.TouchEvents.InRange), - Primary = ti.dwFlags.HasFlag(NativeMethods.TouchEvents.Primary), - NoCoalesce = ti.dwFlags.HasFlag(NativeMethods.TouchEvents.NoCoalesce), - Palm = ti.dwFlags.HasFlag(NativeMethods.TouchEvents.Palm) - }; - - handler(sender, te); - - m.Result = new IntPtr(1); // Indicate to Windows that the message was handled - } + var args = new RotateGestureEventArgs + { + LocationX = location.X, + LocationY = location.Y, + Flags = flags, + SequenceId = gi.dwSequenceID, + Angle = ArgToRadians(gi.ullArguments) + }; + onRotate(sender, args); + } + break; + + case NativeMethods.GestureIdTwoFingerTap: + if (onTap != null) + { + var args = new TapGestureEventArgs + { + LocationX = location.X, + LocationY = location.Y, + Flags = flags, + SequenceId = gi.dwSequenceID + }; + onTap(sender, args); + } + break; + + case NativeMethods.GestureIdPressAndTap: + if (onPressAndTap != null) + { + var args = new PressAndTapGestureEventArgs + { + LocationX = location.X, + LocationY = location.Y, + Flags = flags, + SequenceId = gi.dwSequenceID + }; + onPressAndTap(sender, args); + } + break; } - NativeMethods.CloseTouchInputHandle(m.LParam); + return true; + } + + /// + /// Converts from "binary radians" to traditional radians. + /// + private static double ArgToRadians(long arg) + { + return ((arg / 65535.0) * 4.0 * Math.PI) - 2.0 * Math.PI; } }