Gestures on touchscreen changed from Rhino5 to 6-7

Hi @Matteoz,

From what I recall, the viewports in Rhino 5 32-bit did not support any touch gestures. This is because Rhino 5 32-bit ran on Windows XP, and this version of Window did not support gestures. Since there is no gesture support, any touch event is just translated to a simple mouse event. Thus, touching an object in a viewport would select it, for example.

We hooked up basic gesture support for viewports in Rhino 5 64-bit, and this support has not changed in Rhino 6 and Rhino 7. The gesture support consists of viewport manipulation (e.g. pan, zoom, and rotate).

No there is not, sorry.

– Dale

Ok this is a huge issue for us. Since we no longer can use Rhino5 32 bit. And we need to use the touchscreen.
In the next release, can you expose a property to disable this hook you did for gestures in 64 bit version? So we can keep using our gestures implementation.
Or can you suggest any other way to select and move the objects inside the views using the touchscreen?

Hi @Matteoz,

The viewport gestures have been in Rhino since 2013. So I’m somewhat surprised your just learning about this. But then again, maybe I’m not (surprised).

Can you tell me why the old behavior is important to you? Are you really modeling in Rhino with a touch screen?

Providing some way of disabling viewport gestures will not be easy. So before investing a bunch of development effort, I want to better understand why this is important to you.

Thanks,

– Dale

My personal laptop can be folded up and used as a tablet, if I could use Rhino and GH in tablet mode I would do it to work lying down or half lying down quite a lot. The mouse forces you to sit down and is very uncomfortable to use on non-hard surfaces. Another possible application is to use Rhino to show a design to a client, where the tablet experience is much more enjoyable anywhere than having to sit at a desk which is normally intended for one person.

It would be really great to be able to use GH/RH with a pen.

Hi @dale sorry for the delay (holidays). No need to be surprised. My firm simply never allocated the time to upgrade rhinoceros and when another person tried years ago once he faces this gesture problem simply gave up. So here I am now upgrading from rhino5 to 7 because the licenses will no more be available.

Anyway, the plugin runs on a machine where the user is a worker and there is no mouse and no keyboard since he can’t use them, so everything works using the touch screen. We are not modeling. Usually, they import some draw or use some other tool to create geometries. We need the old (rhino5 32) behavior to be able to select items with a single touch and to move them.

Hi @Matteoz,

Thanks for the details.

I’ve logged your request. You can track it here:

https://mcneel.myjetbrains.com/youtrack/issue/RH-62173

– Dale

Thanks, I’ll track it.

We may also need a registry key to turn it off, in this way we can set it while installing the plugin with a setup.

RH-62173 is fixed in the latest Rhino 7 Service Release Candidate

1 Like

Thank you. I’ve tested it. It works.

It would be great if the fix could be implemented for 6 too. I also would like to regain the ability to select like in v5.

I too would appreciate an update for V6 to disable gesture

The modification added to Rhino 7 (https://mcneel.myjetbrains.com/youtrack/issue/RH-62173) will not be merged down to Rhino 6 - sorry.

– Dale

Hi,
I’m testing the touch features of Rhinoceros 7.16 from RhinoCommon with the effect of the parameter EnableWindowsGestures and I’ve found that

  • If I set this parameter to true the native drawing features do not work but the zoom/pan works from the touch screen as defined previously in this thread (for example drawing a sketch with deviceDown/dragToDefineCurve/deviceUp works only from mouse)
  • If I set this parameter to false the native drawing features return to work and the touch gestures are now disabled (the sketch example now works drawn with both mouse and touch devices)

Could it be possible to have both the features when it is true and only the drawing feature when it is false ? In this way this parameter defines only the enable of touch gestures handled from Rhinoceros.

A more subtle problem is that the WndProc message chain from Windows API hooked to Rhinoceros 7 IntPtr Window Handle exposes the message WM_GESTURES=281 with the begin and end events, but no internal info of the gestures. The same code linked to Rhinoceros 5 exposes the internal info of the zoom, pan or rotate gestures as parsed from Windows before arriving (SetGestureConfig function (winuser.h) - Win32 apps | Microsoft Docs and GESTURECONFIG (winuser.h) - Win32 apps | Microsoft Docs). If the parameter is false, the client plugins should be able to hook on these events to implements their version of the gesture, to transform their macro-objects from the gesture event defined by the user.

Hi,

I still have the Windows Procedure message 281 that gives {begin ; parsed content by Windows at each instant ; end} events in Rhinoceros 5.14 and only {begin ; end} in Rhinoceros 7.16.

I have disabled the parameter described in this thread, given that I’d like to have the Windows native behavior of the message 281, and the Rhinoceros commands runned by its UI or its scripts by tapping the screen.

This is a critical feature on the plugins I develop. I have reported below the comparison with the code I have around my plugins to handle this feature. If you need more info let me know.

The link is made with DllImport and Rhinocommon in .NET 4.8 as the following signatures:

public struct GESTUREINFO
{

public uint cbSize;
public uint dwFlags;
public uint dwID;
public IntPtr hwndTarget;
public POINTS ptsLocation;
public uint dwInstanceID;
public uint dwSequenceID;
public ulong ullArguments;
public uint cbExtraArgs;

}

internal static class User32
{

public const int WM_NCDESTROY = 130;
public const int GWLP_WNDPROC = -4;
public const int WM_TOUCH = 576;
public const int TOUCHEVENTF_MOVE = 1;
public const int TOUCHEVENTF_DOWN = 2;
public const int TOUCHEVENTF_UP = 4;
public const int TOUCHEVENTF_INRANGE = 8;
public const int TOUCHEVENTF_PRIMARY = 16;
public const int TOUCHEVENTF_NOCOALESCE = 32;
public const int TOUCHEVENTF_PEN = 64;
public const int TOUCHEVENTF_PALM = 128;
public const int TOUCHINPUTMASKF_TIMEFROMSYSTEM = 1;
public const int TOUCHINPUTMASKF_EXTRAINFO = 2;
public const int TOUCHINPUTMASKF_CONTACTAREA = 4;
public const uint GC_ALLGESTURES = 1;
public const uint WM_GESTURE = 281;
public const uint WM_GESTURENOTIFY = 282;
public const uint GF_BEGIN = 1;
public const uint GF_INERTIA = 2;
public const uint GF_END = 4;
public const uint GID_BEGIN = 1;
public const uint GID_END = 2;
public const uint GID_ZOOM = 3;
public const uint GID_PAN = 4;
public const uint GID_ROTATE = 5;
public const uint GID_TWOFINGERTAP = 6;
public const uint GID_PRESSANDTAP = 7;

[DllImport("user32")]
public static extern bool SetProcessDPIAware();

[DllImport("user32")]
public static extern bool IsWindow(IntPtr hWnd);

[DllImport("user32")]
public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);

[DllImport("user32", EntryPoint = "SetWindowLongPtr")]
public static extern IntPtr SubclassWindow64(
  IntPtr hWnd,
  int nIndex,
  User32.WindowProcDelegate dwNewLong);

[DllImport("user32", EntryPoint = "SetWindowLong")]
public static extern IntPtr SubclassWindow(
  IntPtr hWnd,
  int nIndex,
  User32.WindowProcDelegate dwNewLong);

[DllImport("user32")]
public static extern uint CallWindowProc(
  IntPtr prevWndFunc,
  IntPtr hWnd,
  int msg,
  IntPtr wparam,
  IntPtr lparam);

[DllImport("user32", EntryPoint = "GetSystemMetrics")]
public static extern int GetDigitizerCapabilities(User32.DigitizerIndex index);

[DllImport("user32")]
public static extern bool RegisterTouchWindow(IntPtr hWnd, User32.TouchWindowFlag flags);

[DllImport("user32")]
public static extern bool UnregisterTouchWindow(IntPtr hWnd);

[DllImport("user32")]
public static extern bool IsTouchWindow(IntPtr hWnd, out uint ulFlags);

[DllImport("user32")]
public static extern bool GetTouchInputInfo(
  IntPtr hTouchInput,
  int cInputs,
  [In, Out] TOUCHINPUT[] pInputs,
  int cbSize);

[DllImport("user32")]
public static extern void CloseTouchInputHandle(IntPtr lParam);

[DllImport("user32")]
public static extern bool SetProp(IntPtr hWnd, string lpString, IntPtr hData);

public static ushort LoWord(uint number) => (ushort) (number & (uint) ushort.MaxValue);

public static ushort HiWord(uint number) => (ushort) (number >> 16 & (uint) ushort.MaxValue);

public static uint LoDWord(ulong number) => (uint) (number & (ulong) uint.MaxValue);

public static uint HiDWord(ulong number) => (uint) (number >> 32 & (ulong) uint.MaxValue);

public static short LoWord(int number) => (short) number;

public static short HiWord(int number) => (short) (number >> 16);

public static int LoDWord(long number) => (int) number;

public static int HiDWord(long number) => (int) (number >> 32);

[DllImport("user32")]
public static extern bool SetGestureConfig(
  IntPtr hwnd,
  uint dwReserved,
  uint cIDs,
  GESTURECONFIG[] pGestureConfig,
  uint cbSize);

[DllImport("user32")]
public static extern bool GetGestureInfo(IntPtr hGestureInfo, ref GESTUREINFO pGestureInfo);

public static ushort GID_ROTATE_ANGLE_TO_ARGUMENT(ushort arg) => (ushort) (((double) arg + 6.2831853) / 12.5663706 * (double) ushort.MaxValue);

public static double GID_ROTATE_ANGLE_FROM_ARGUMENT(ushort arg) => (double) arg / (double) ushort.MaxValue * 4.0 * 3.14159265 - 6.2831853;

[DllImport("user32")]
public static extern bool CloseGestureInfoHandle(IntPtr hGestureInfo);

public delegate uint WindowProcDelegate(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam);

public enum DigitizerIndex
{
  SM_DIGITIZER = 94, // 0x0000005E
  SM_MAXIMUMTOUCHES = 95, // 0x0000005F
}

public enum TouchWindowFlag : uint
{
  FineTouch = 1,
  WantPalm = 2,
}

public struct GESTURENOTIFYSTRUCT
{
  public uint cbSize;
  public uint dwFlags;
  public IntPtr hwndTarget;
  public POINTS ptsLocation;
  public uint dwInstanceID;
}

}

public abstract class Handler
{

private readonly IHwndWrapper _hWndWrapper;
private static List<object> _controlInUse = new List<object>();
private User32.WindowProcDelegate _windowProcDelegate;
private IntPtr _originalWindowProcId;

protected abstract bool SetHWndTouchInfo();

protected abstract uint WindowProc(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam);

internal static T CreateHandler<T>(IHwndWrapper hWndWrapper) where T : Handler
{
  if (Handler._controlInUse.Contains(hWndWrapper.Source))
    throw new Exception("Only one handler can be registered for a control.");
  hWndWrapper.HandleDestroyed += (EventHandler) ((s, e) => Handler._controlInUse.Remove(s));
  Handler._controlInUse.Add(hWndWrapper.Source);
  return Activator.CreateInstance(typeof (T), BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.CreateInstance, (Binder) null, new object[1]
  {
    (object) hWndWrapper
  }, Thread.CurrentThread.CurrentCulture) as T;
}

internal Handler(IHwndWrapper hWndWrapper)
{
  this._hWndWrapper = hWndWrapper;
  if (this._hWndWrapper.IsHandleCreated)
    this.Initialize();
  else
    this._hWndWrapper.HandleCreated += (EventHandler) ((s, e) => this.Initialize());
}

private void Initialize()
{
  if (!this.SetHWndTouchInfo())
    throw new NotSupportedException("Cannot register window");
  this._windowProcDelegate = new User32.WindowProcDelegate(this.WindowProcSubClass);
  this._originalWindowProcId = IntPtr.Size == 4 ? User32.SubclassWindow(this._hWndWrapper.Handle, -4, this._windowProcDelegate) : User32.SubclassWindow64(this._hWndWrapper.Handle, -4, this._windowProcDelegate);
  using (Graphics graphics = Graphics.FromHwnd(this._hWndWrapper.Handle))
  {
    this.DpiX = graphics.DpiX;
    this.DpiY = graphics.DpiY;
  }
  this.WindowMessage += (EventHandler<WMEventArgs>) ((s, e) => {});
}

private uint WindowProcSubClass(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam)
{
  this.WindowMessage((object) this, new WMEventArgs(hWnd, msg, wparam, lparam));
  if (msg == 282 && this.GestureNotify != null)
  {
    this.GestureNotify((object) this, new GestureNotifyEventArgs(lparam));
  }
  else
  {
    uint num = this.WindowProc(hWnd, msg, wparam, lparam);
    if (num != 0U)
      return num;
  }
  return User32.CallWindowProc(this._originalWindowProcId, hWnd, msg, wparam, lparam);
}

internal IHwndWrapper HWndWrapper => this._hWndWrapper;

protected IntPtr ControlHandle => !this._hWndWrapper.IsHandleCreated ? IntPtr.Zero : this._hWndWrapper.Handle;

public float DpiX { get; private set; }

public float DpiY { get; private set; }

public event EventHandler<GestureNotifyEventArgs> GestureNotify;

internal event EventHandler<WMEventArgs> WindowMessage;

public static bool IsTouchWindows(IntPtr hWnd) => User32.IsTouchWindow(hWnd, out uint _);

public static class DigitizerCapabilities
{
  public static DigitizerStatus Status => (DigitizerStatus) User32.GetDigitizerCapabilities(User32.DigitizerIndex.SM_DIGITIZER);

  public static int MaxumumTouches => User32.GetDigitizerCapabilities(User32.DigitizerIndex.SM_MAXIMUMTOUCHES);

  public static bool IsIntegratedTouch => (Handler.DigitizerCapabilities.Status & DigitizerStatus.IntegratedTouch) != (DigitizerStatus) 0;

  public static bool IsExternalTouch => (Handler.DigitizerCapabilities.Status & DigitizerStatus.ExternalTouch) != (DigitizerStatus) 0;

  public static bool IsIntegratedPan => (Handler.DigitizerCapabilities.Status & DigitizerStatus.IntegratedPan) != (DigitizerStatus) 0;

  public static bool IsExternalPan => (Handler.DigitizerCapabilities.Status & DigitizerStatus.ExternalPan) != (DigitizerStatus) 0;

  public static bool IsMultiInput => (Handler.DigitizerCapabilities.Status & DigitizerStatus.MultiInput) != (DigitizerStatus) 0;

  public static bool IsStackReady => (Handler.DigitizerCapabilities.Status & DigitizerStatus.StackReady) != (DigitizerStatus) 0;

  public static bool IsMultiTouchReady => (Handler.DigitizerCapabilities.Status & (DigitizerStatus.MultiInput | DigitizerStatus.StackReady)) != (DigitizerStatus) 0;
}

}

public class GestureHandler : Handler
{

	private static readonly EventHandler<GestureEventArgs> _emptyFunc = (EventHandler<GestureEventArgs>)((s, e) => { });
	private readonly Dictionary<uint, EventHandler<GestureEventArgs>> _eventMap = new Dictionary<uint, EventHandler<GestureEventArgs>>()
{
  {
	GestureHandler.EventMapID.Begin,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.End,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.PanBegin,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.Pan,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.PanEnd,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.PressAndTap,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.RotateBegin,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.Rotate,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.RotateEnd,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.TwoFingerTap,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.ZoomBegin,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.Zoom,
	GestureHandler._emptyFunc
  },
  {
	GestureHandler.EventMapID.ZoomEnd,
	GestureHandler._emptyFunc
  }
};

	private static uint MapWM2EventId(uint dwID, uint dwFlags) => (uint)(((int)dwID << 3) + (dwID == 6U || dwID == 7U || (dwID == 1U || dwID == 2U) ? 0 : (int)dwFlags & 5));

	internal GestureHandler(IHwndWrapper hWndWrapper)
	  : base(hWndWrapper)
	{
	}

	protected override bool SetHWndTouchInfo()
	{
		var gestureConfig = new GESTURECONFIG[]
		{
			new GESTURECONFIG() { dwID = 3U, dwWant = 1U, dwBlock = 0U },
			new GESTURECONFIG() { dwID = 4U, dwWant = 1U, dwBlock = 0U },
			new GESTURECONFIG() { dwID = 5U, dwWant = 1U, dwBlock = 0U },
			new GESTURECONFIG() { dwID = 6U, dwWant = 1U, dwBlock = 0U }
		};

		var result = User32.SetGestureConfig(
			this.ControlHandle,
			0U,
			(uint)gestureConfig.Length,
			gestureConfig,
			(uint)Marshal.SizeOf(typeof(GESTURECONFIG)));

		return result;
	}

	internal GestureEventArgs LastBeginEvent { get; set; }

	internal GestureEventArgs LastEvent { get; set; }

	public event EventHandler<GestureEventArgs> Begin
	{
		add => this._eventMap[GestureHandler.EventMapID.Begin] += value;
		remove => this._eventMap[GestureHandler.EventMapID.Begin] -= value;
	}

	public event EventHandler<GestureEventArgs> End
	{
		add => this._eventMap[GestureHandler.EventMapID.End] += value;
		remove => this._eventMap[GestureHandler.EventMapID.End] -= value;
	}

	public event EventHandler<GestureEventArgs> PanBegin
	{
		add => this._eventMap[GestureHandler.EventMapID.PanBegin] += value;
		remove => this._eventMap[GestureHandler.EventMapID.PanBegin] -= value;
	}

	public event EventHandler<GestureEventArgs> Pan
	{
		add => this._eventMap[GestureHandler.EventMapID.Pan] += value;
		remove => this._eventMap[GestureHandler.EventMapID.Pan] -= value;
	}

	public event EventHandler<GestureEventArgs> PanEnd
	{
		add => this._eventMap[GestureHandler.EventMapID.PanEnd] += value;
		remove => this._eventMap[GestureHandler.EventMapID.PanEnd] -= value;
	}

	public event EventHandler<GestureEventArgs> PressAndTap
	{
		add => this._eventMap[GestureHandler.EventMapID.PressAndTap] += value;
		remove => this._eventMap[GestureHandler.EventMapID.PressAndTap] -= value;
	}

	public event EventHandler<GestureEventArgs> RotateBegin
	{
		add => this._eventMap[GestureHandler.EventMapID.RotateBegin] += value;
		remove => this._eventMap[GestureHandler.EventMapID.RotateBegin] -= value;
	}

	public event EventHandler<GestureEventArgs> Rotate
	{
		add => this._eventMap[GestureHandler.EventMapID.Rotate] += value;
		remove => this._eventMap[GestureHandler.EventMapID.Rotate] -= value;
	}

	public event EventHandler<GestureEventArgs> RotateEnd
	{
		add => this._eventMap[GestureHandler.EventMapID.RotateEnd] += value;
		remove => this._eventMap[GestureHandler.EventMapID.RotateEnd] -= value;
	}

	public event EventHandler<GestureEventArgs> TwoFingerTap
	{
		add => this._eventMap[GestureHandler.EventMapID.TwoFingerTap] += value;
		remove => this._eventMap[GestureHandler.EventMapID.TwoFingerTap] -= value;
	}

	public event EventHandler<GestureEventArgs> ZoomBegin
	{
		add => this._eventMap[GestureHandler.EventMapID.ZoomBegin] += value;
		remove => this._eventMap[GestureHandler.EventMapID.ZoomBegin] -= value;
	}

	public event EventHandler<GestureEventArgs> Zoom
	{
		add => this._eventMap[GestureHandler.EventMapID.Zoom] += value;
		remove => this._eventMap[GestureHandler.EventMapID.Zoom] -= value;
	}

	public event EventHandler<GestureEventArgs> ZoomEnd
	{
		add => this._eventMap[GestureHandler.EventMapID.ZoomEnd] += value;
		remove => this._eventMap[GestureHandler.EventMapID.ZoomEnd] -= value;
	}

	protected override uint WindowProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
	{
		if (msg != 281)
		{
			return 0;
		}

		GESTUREINFO gestureinfo = new GESTUREINFO()
		{
			cbSize = (uint)Marshal.SizeOf(typeof(GESTUREINFO))
		};

		GestureEventArgs e = User32.GetGestureInfo(lParam, ref gestureinfo) ? new GestureEventArgs(this, ref gestureinfo) : throw new Exception("Cannot get gesture information");
		
		try
		{
			var mapWM2EventId = GestureHandler.MapWM2EventId(gestureinfo.dwID, gestureinfo.dwFlags);

			this._eventMap[mapWM2EventId]((object)this, e);

			var message = string.Format(
				"time='{0}' mapWM2EventId='{1}'",
				DateTime.Now.ToLongTimeString(),
				mapWM2EventId
				);

			System.Diagnostics.Trace.WriteLine(message);
		}
		catch (ArgumentOutOfRangeException ex)
		{
		}
		this.LastEvent = e;
		if (e.IsBegin)
			this.LastBeginEvent = e;
		if (gestureinfo.dwID != 6U && gestureinfo.dwID != 7U)
			return 0;
		User32.CloseGestureInfoHandle(lParam);
		return 1;
	}

	private static class EventMapID
	{
		public static readonly uint Begin = GestureHandler.MapWM2EventId(1U, 0U);
		public static readonly uint End = GestureHandler.MapWM2EventId(2U, 0U);
		public static readonly uint PanBegin = GestureHandler.MapWM2EventId(4U, 1U);
		public static readonly uint Pan = GestureHandler.MapWM2EventId(4U, 0U);
		public static readonly uint PanEnd = GestureHandler.MapWM2EventId(4U, 4U);
		public static readonly uint PressAndTap = GestureHandler.MapWM2EventId(7U, 0U);
		public static readonly uint RotateBegin = GestureHandler.MapWM2EventId(5U, 1U);
		public static readonly uint Rotate = GestureHandler.MapWM2EventId(5U, 0U);
		public static readonly uint RotateEnd = GestureHandler.MapWM2EventId(5U, 4U);
		public static readonly uint TwoFingerTap = GestureHandler.MapWM2EventId(6U, 0U);
		public static readonly uint ZoomBegin = GestureHandler.MapWM2EventId(3U, 1U);
		public static readonly uint Zoom = GestureHandler.MapWM2EventId(3U, 0U);
		public static readonly uint ZoomEnd = GestureHandler.MapWM2EventId(3U, 4U);
	}
}

public class GestureEventArgs : EventArgs
{

private readonly uint _dwFlags;

internal GestureEventArgs(GestureHandler handler, ref GESTUREINFO gestureInfo)
{
  this._dwFlags = gestureInfo.dwFlags;
  this.GestureId = gestureInfo.dwID;
  this.GestureArguments = gestureInfo.ullArguments;
  this.LastEvent = handler.LastEvent;
  this.LastBeginEvent = handler.LastBeginEvent;
  this.DecodeGesture(handler.HWndWrapper, ref gestureInfo);
  if (!this.IsBegin)
    return;
  this.LastBeginEvent = (GestureEventArgs) null;
  this.LastEvent = (GestureEventArgs) null;
}

private void DecodeGesture(IHwndWrapper hWndWrapper, ref GESTUREINFO gestureInfo)
{
  this.Location = hWndWrapper.PointToClient(new Point((int) gestureInfo.ptsLocation.x, (int) gestureInfo.ptsLocation.y));
  this.Center = this.Location;
  switch (this.GestureId)
  {
    case 3:
      Point point = this.IsBegin ? this.Location : this.LastBeginEvent.Location;
      this.Center = new Point((this.Location.X + point.X) / 2, (this.Location.Y + point.Y) / 2);
      this.ZoomFactor = this.IsBegin ? 1.0 : (double) gestureInfo.ullArguments / (double) this.LastEvent.GestureArguments;
      break;
    case 4:
      this.PanTranslation = this.IsBegin ? new Size(0, 0) : new Size(this.Location.X - this.LastEvent.Location.X, this.Location.Y - this.LastEvent.Location.Y);
      int number = User32.HiDWord((long) gestureInfo.ullArguments);
      this.PanVelocity = new Size((int) User32.LoWord(number), (int) User32.HiWord(number));
      break;
    case 5:
      ushort num = this.IsBegin ? (ushort) 0 : (ushort) this.LastEvent.GestureArguments;
      this.RotateAngle = User32.GID_ROTATE_ANGLE_FROM_ARGUMENT((ushort) (gestureInfo.ullArguments - (ulong) num));
      break;
  }
}

public uint GestureId { get; private set; }

public ulong GestureArguments { get; private set; }

public Point Location { get; private set; }

public bool IsBegin => ((int) this._dwFlags & 1) != 0;

public bool IsEnd => ((int) this._dwFlags & 4) != 0;

public bool IsInertia => ((int) this._dwFlags & 2) != 0;

public double RotateAngle { get; private set; }

public Point Center { get; private set; }

public double ZoomFactor { get; private set; }

public Size PanTranslation { get; private set; }

public Size PanVelocity { get; private set; }

public GestureEventArgs LastBeginEvent { get; internal set; }

public GestureEventArgs LastEvent { get; internal set; }

}

main event in GestureHandler class:
`

protected static uint WindowProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
{
if (msg != 281)
return 0;
GESTUREINFO gestureinfo = new GESTUREINFO()
{
cbSize = (uint) Marshal.SizeOf(typeof (GESTUREINFO))
};
GestureEventArgs e = User32.GetGestureInfo(lParam, ref gestureinfo) ? new GestureEventArgs(this, ref gestureinfo) : throw new Exception(“Cannot get gesture information”);

this.LastEvent = e;
if (e.IsBegin)
this.LastBeginEvent = e;
if (gestureinfo.dwID != 6U && gestureinfo.dwID != 7U)
return 0;
User32.CloseGestureInfoHandle(lParam);
return 1;}
`

initialization in GestureHandler class:

private bool SetGesturesDesired()
{
var gestureConfig = new GESTURECONFIG
{
new GESTURECONFIG() { dwID = 3U, dwWant = 1U, dwBlock = 0U },
new GESTURECONFIG() { dwID = 4U, dwWant = 1U, dwBlock = 0U },
new GESTURECONFIG() { dwID = 5U, dwWant = 1U, dwBlock = 0U },
new GESTURECONFIG() { dwID = 6U, dwWant = 1U, dwBlock = 0U }
};
var result = User32.SetGestureConfig(
this.ControlHandle,
0U,
(uint)gestureConfig.Length,
gestureConfig,
(uint)Marshal.SizeOf(typeof(GESTURECONFIG)));
return result;
}

private void Initialize()
{
var mainWindowHandle = Rhino.RhinoApp.MainWindowHandle();
if (!SetGesturesDesired())
throw new NotSupportedException(“Cannot register window”);
this._windowProcDelegate = new User32.WindowProcDelegate(this.WindowProcSubClass);
this._originalWindowProcId = IntPtr.Size == 4 ? User32.SubclassWindow(mainWindowHandle, -4, this._windowProcDelegate) : User32.SubclassWindow64(mainWindowHandle, -4, this._windowProcDelegate);
using (Graphics graphics = Graphics.FromHwnd(mainWindowHandle))
{
this.DpiX = graphics.DpiX;
this.DpiY = graphics.DpiY;
}
this.WindowMessage += (EventHandler) ((s, e) => {});
}

I have tried the same gestures on the same pc with Rhinoceros 5.14 compared to Rhinoceros 7.16 and the results are reported below:

Rhinoceros 7.16 one zoom gesture (down index and thumb, drag index and thumb closing their distance, up index and thumb)

03/06/2022 11:39:40.120 DEBUG: time=‘11:39:40’ mapWM2EventId=‘8’

03/06/2022 11:39:40.581 DEBUG: time=‘11:39:40’ mapWM2EventId=‘16’

Rhinoceros 7.16 one rotate gesture (down index and thumb, rotate index and thumb reciprocally, up index and thumb)

03/06/2022 11:40:10.810 DEBUG: time=‘11:40:10’ mapWM2EventId=‘8’

03/06/2022 11:40:11.567 DEBUG: time=‘11:40:11’ mapWM2EventId=‘16’

Rhinoceros 7.16 one pan gesture (down index and medium, drag index and medium equally, up index and medium)

03/06/2022 11:40:36.678 DEBUG: time=‘11:40:36’ mapWM2EventId=‘8’

03/06/2022 11:40:37.103 DEBUG: time=‘11:40:37’ mapWM2EventId=‘16’

Rhinoceros 5.14 one zoom gesture

03/06/2022 10:48:26.666 DEBUG: time=‘10:48:26’ mapWM2EventId=‘8’

03/06/2022 10:48:26.666 DEBUG: time=‘10:48:26’ mapWM2EventId=‘25’

03/06/2022 10:48:26.666 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.681 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.681 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.744 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.759 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.759 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.759 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.775 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.806 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.806 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.806 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.806 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.806 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.837 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.837 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.837 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.837 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.853 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.853 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.853 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.853 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.884 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.884 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.884 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.884 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.900 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.900 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.900 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.900 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.916 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.932 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.932 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.932 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.962 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.962 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.978 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.978 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.994 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.994 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.994 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:26.994 DEBUG: time=‘10:48:26’ mapWM2EventId=‘24’

03/06/2022 10:48:27.009 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.025 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.025 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.025 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.040 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.040 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.040 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.040 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.072 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.072 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.072 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.072 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.103 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.119 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.119 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.150 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.150 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.181 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.197 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.212 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.228 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.259 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.306 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.353 DEBUG: time=‘10:48:27’ mapWM2EventId=‘24’

03/06/2022 10:48:27.385 DEBUG: time=‘10:48:27’ mapWM2EventId=‘28’

03/06/2022 10:48:27.385 DEBUG: time=‘10:48:27’ mapWM2EventId=‘16’

Rhinoceros 5.14 one rotate gesture

03/06/2022 10:49:56.172 DEBUG: time=‘10:49:56’ mapWM2EventId=‘8’

03/06/2022 10:49:56.204 DEBUG: time=‘10:49:56’ mapWM2EventId=‘41’

03/06/2022 10:49:56.250 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.282 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.329 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.360 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.406 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.438 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.454 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.484 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.516 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.532 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.563 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.579 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.609 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.625 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.656 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.672 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.703 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.719 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.735 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.766 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.797 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.813 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.844 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.860 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.891 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.906 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.937 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.953 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:56.984 DEBUG: time=‘10:49:56’ mapWM2EventId=‘40’

03/06/2022 10:49:57.001 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.032 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.047 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.079 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.094 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.125 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.157 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.172 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.203 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.219 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.250 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.266 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.313 DEBUG: time=‘10:49:57’ mapWM2EventId=‘40’

03/06/2022 10:49:57.344 DEBUG: time=‘10:49:57’ mapWM2EventId=‘44’

03/06/2022 10:49:57.344 DEBUG: time=‘10:49:57’ mapWM2EventId=‘16’

Rhinoceros 5.14 one pan gesture

03/06/2022 11:11:41.307 DEBUG: time=‘11:11:41’ mapWM2EventId=‘8’

03/06/2022 11:11:41.314 DEBUG: time=‘11:11:41’ mapWM2EventId=‘33’

03/06/2022 11:11:41.324 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.337 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.366 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.382 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.413 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.432 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.437 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.443 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.460 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.466 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.485 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.491 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.515 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.520 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.539 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.545 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.557 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.563 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.576 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.581 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.607 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.622 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.627 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.644 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.656 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.667 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.674 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.692 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.698 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.716 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.765 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.796 DEBUG: time=‘11:11:41’ mapWM2EventId=‘32’

03/06/2022 11:11:41.815 DEBUG: time=‘11:11:41’ mapWM2EventId=‘36’

03/06/2022 11:11:41.820 DEBUG: time=‘11:11:41’ mapWM2EventId=‘16’

Hi @cavalli.stefano,

Sorry - I don’t have any idea how to help…

– Dale

These are intrinsic features of how the executable is implemented and how it handles these messages, in my opinion. Perhaps with the multi-platform features of Rhinoceros 6 and Rhinoceros 7 you have handled a Windows message that was previously available to Rhinoceros Plugins ?

Are you sure that Rhinoceros 7 and Rhinoceros 5 have the same handling of win32 messages? The code reported in my last post is working on the Rhinoceros Window, without adding additional UI; let me know if you need a plugin that replicates this feature so you can see the difference between Rhinoceros 5 and Rhinoceros 7.

TestGesturesOnNativeWindowCommand.7z (7.2 KB)

I have made an example to replicate the problem, inside there is a VS2019 solution with 2 projects linking the same code:

  • a Rhinoceros 5.12 plugin
  • a Rhinoceros 7.18 plugin

If the user activates the events with the command TestGesturesOnNativeWindowCommand, the parsed gestures are print each instant with the increment on the console.
Personally I’m interested in only these 3 gestures:

  • zoom gesture (down index and thumb, drag index and thumb closing their distance, up index and thumb)
  • rotate gesture (down index and thumb, rotate index and thumb reciprocally, up index and thumb)
  • pan gesture (down index and medium, drag index and medium equally, up index and medium)

The plugin crashes after a while and I haven’t understood why, but the console shows the result in Rhinoceros 5 and nothing in Rhinoceros 7.

The parameter introduced with this topic must be setted to false to allow the native UI commands (for example the _-Sketch command).

In Rhinoceros 5 both these gestures and the Rhinoceros native commands work.

Let me know if you need more info

Hello @cavalli.stefano , I read your comment, and it seems the touch gesture either for drawing and zoom-rotate-pan, is solved. However, I am not really clear and understand about your explanation. I downloaded your file, but I am not sure where should I start with.

if you do not mind, could you please explain more about the steps?
Thank you so much for your help.

Hello @Matteoz , I just wondering if you can use both for drawing and zoom-rotate-pan function in the same time with hand gesture (touch screen) without mouse?

The purpose of the Microsoft Visual Studio 2019 .NET solution is only to log in the Rhinoceros 5 and Rhinoceros 7 environment the gestures parsed from Microsoft Windows OS on the .NET environment of the Rhinoceros Plugin.

In the sample there is the same code that creates the events of the user acting on the touch screen on Rhinoceros 5 as
start
incremental change
incremental change
.
.
.
incremental change
incremental change
end

and the same code in Rhinoceros 7 gives only
start
end

If you know how to access these events from .NET linking directly the native Rhinoceros Window in its whole area let me know. My purpose is to move some objects selected by the user with a logic defined by my plugin