本文从代码底层告诉大家,在触摸屏幕之后是如何拿到触摸点并且转换为事件

在 WPF 界面框架核心就是交互和渲染,触摸是交互的一部分。在 WPF 是需要使用多个线程来做触摸和渲染,触摸是单独一个线程,这个线程就是只获得触摸,而将触摸转路由是在主线程。

在触摸线程各个模块的关系请看下面

从触摸线程转换到主线程,然后从主线程封装为路由事件的模块请看下面

路由事件需要封装触摸消息并且找到命中的元素

实际上看到这里,整个触摸就告诉了大家过程,从大的方面已经可以知道过程,触摸是如何转路由。具体代码是如何做的请看下面

在 WPF 需要使用一个线程去获取触摸的信息,这个线程是在 PenThreadWorker 创建,在 PenThreadWorker 的构造函数有下面代码

			new Thread(new ThreadStart(this.ThreadProc))
			{
				IsBackground = true
			}.Start();

通过这个方法就可以创建线程运行 ThreadProc 这个函数是一个无限循环,请看代码

while (!this.__disposed)
{
	// 忽略代码
}

这个函数的底层实际上是包括了另一个循环来从 penimc2_v0400.dll 拿到触摸信息

如果插入的设备有一个,就会运行 penimc2_v0400.dllGetPenEvent 拿到触摸的信息。如果有多个就会调用 GetPenEventMultiple 方法。这里先不用理会底层的实现,只需要知道底层实现的代码大概就是下面代码

		internal void ThreadProc()
		{
			Thread.CurrentThread.Name = "Stylus Input";
			while (!this.__disposed)
			{
				// 初始化的代码
				for (;;)
				{
					// 有一个触摸设备,就进入 GetPenEvent 拿到触摸
					if (this._handles.Length == 1)
					{
						UnsafeNativeMethods.GetPenEvent(一些参数)
						num2 = 0;
					}
					else
					{
						// 有多个触摸设备,就会进入 GetPenEventMultiple 在用户触摸
						UnsafeNativeMethods.GetPenEventMultiple(参数)
					}
					PenContext penContext = xx; // 在触摸对应的 PenContext 是通过判断用户触摸
				
					this.FireEvent(penContext, 参数);
				}
			}
		}

通过这个方法可以知道触摸的设备的 id 和触摸的数据,触摸的事件

在拿到触摸信息之后,会调用 FireEvent 转换事件,在拿到的信息包括了表示是什么事件,因为触摸的事件是传入一个数值,需要通过这个数值转换为对应的事件

  • 707:PenInRange
  • 708:PenOutOfRange
  • 709:PenDown
  • 710:PenUp
  • 711:Packets

如收到的是 709 事件,就会进入 FireEvent 在下面代码使用 penContext.FirePenDown 告诉现在是触摸按下

在 FirePenDown 函数会先判断这个触摸是否初始化,每个触摸都有 StylusPointDescription 这个值是使用 IPimcContext2 获取 GetPacketPropertyInfo 拿到,是触摸屏的设备描述信息里告诉程序这个触摸的精度和触摸宽度

penContext 传入事件给 PenContexts.OnPenDown 这个是在 PenContexts 的三个主要事件的一个,可以从代码知道都是调用 ProcessInput 只是第一个参数不相同

		internal void OnPenDown(PenContext penContext, int tabletDeviceId, int stylusPointerId, int[] data, int timestamp)
		{
			// 下面两个函数和这个函数的不同在于 RawStylusActions.Down 其他都是相同
			this.ProcessInput(RawStylusActions.Down, penContext, tabletDeviceId, stylusPointerId, data, timestamp);
		}

		internal void OnPenUp(PenContext penContext, int tabletDeviceId, int stylusPointerId, int[] data, int timestamp)
		{
			this.ProcessInput(RawStylusActions.Up, penContext, tabletDeviceId, stylusPointerId, data, timestamp);
		}

		internal void OnPackets(PenContext penContext, int tabletDeviceId, int stylusPointerId, int[] data, int timestamp)
		{
			this.ProcessInput(RawStylusActions.Move, penContext, tabletDeviceId, stylusPointerId, data, timestamp);
		}

上面代码传入的 stylusPointerId 、data 都是从刚才的底层拿到的值,这里的 tabletDeviceId 是从 PenContext 拿到,一个触摸的设备都有设备的 id 也就是 tabletDeviceId 的值。触摸的时候会给这个触摸一个 id 就是 stylusPointerId 通过这个在进行移动的时候就知道是哪个触摸点在移动。这个技术是用来解决多点触摸,如果用户有多个点触摸,就需要使用 stylusPointerId 来拿到这个点之前的做法

先告诉大家调用的顺序,不然大家看到下面忘记了点就不知道我在说的是什么了,本文图片画的实线表示方法在方法内调用了下一个方法,使用虚线表示在外面调用完成这个方法之后再调用下一个方法。

	
	void A()
	{
		B(); // A和B连接用实线表示
	}

	void C()
	{
		A();
		B(); // A和B连接需要虚线表示 
	}

现在就将事件传入到 ProcessInput 并且告诉 RawStylusActions 这个函数会调用 WispLogic 的 ProcessInput 在这里使用函数的原因是为了传入的时候加上 _inputSource 这里的 WispLogic 可能是 StylusLogic 现在的代码就到了比较熟悉的 StylusLogic 函数

WispLogic 的 Wisp 是 Windows Inking Service Platform 用来在系统和输入设备之间通信

WispLogicProcessInput 会包装输入的参数为