本文记录我读 WPF 源代码的笔记,本文将介绍在 WPF 底层是如何从 Win32 的消息循环里获取到的 WM_POINTER 消息处理转换作为 Touch 事件的参数
由于 WPF 触摸部分会兼顾开启 Pointer 消息和不开启 Pointer 消息,在 WPF 框架里面的逻辑会有部分是兼容逻辑,为了方便大家理解,本文分为两个部分。第一个部分是脱离 WPF 框架,聊聊一个 Win32 程序如何从 Win32 的消息循环获取到的 WM_POINTER 消息处理转换为输入坐标点,以及在触摸下获取触摸信息。第二部分是 WPF 框架是如何安排上这些处理逻辑,如何和 WPF 框架的进行对接
第一部分脱离了 WPF 框架,也就没有了兼容不开启 Pointer 消息的负担,我将使用简单的描述点出关键部分
处理 Pointer 消息
在 Win32 应用程序中,大概有三个方式来进行对 Pointer 消息进行处理。我将从简单到复杂和大家讲述这三个方式
方式1:
接收到 WM_POINTER 消息之后,将 wparam 转换为 pointerId
参数,调用 GetPointerTouchInfo 方法即可获取到 POINTER_INFO 信息
获取 POINTER_INFO 的 ptPixelLocationRaw
字段,即可拿到基于屏幕坐标系的像素点
只需将其转换为窗口坐标系和处理 DPI 即可使用
此方法的最大缺点在于 ptPixelLocationRaw
字段拿到的是丢失精度的点,像素为单位。如果在精度稍微高的触摸屏下,将会有明显的锯齿效果
优点在于其获取特别简单
方式2:
依然是接收到 WM_POINTER 消息之后,将 wparam 转换为 pointerId
参数,调用 GetPointerTouchInfo 方法即可获取到 POINTER_INFO 信息
只是从获取 POINTER_INFO 的 ptPixelLocationRaw
字段换成 ptHimetricLocationRaw
字段
使用 ptHimetricLocationRaw
字段的优势在于可以获取不丢失精度的信息,但需要额外调用 GetPointerDeviceRects 函数获取 displayRect
和 pointerDeviceRect
信息用于转换坐标点,转换逻辑如以下代码所示
以上方式2的代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码
获取代码之后,进入 WPFDemo/DefilireceHowemdalaqu 文件夹,即可获取到源代码
方式2的优点在于可以获取到更高的精度。缺点是相对来说比较复杂,需要多了点点处理
方式3:
此方式会更加复杂,但功能能够更加全面,适合用在要求更高控制的应用里面
先调用 GetPointerDeviceProperties 方法,获取 HID 描述符上报的对应设备属性,此时可以获取到的是具备完全的 HID 描述符属性的方法,可以包括 Windows 的 Pen 协议 里面列举的各个属性,如宽度高度旋转角等信息
收到 WM_POINTER 消息时,调用 GetRawPointerDeviceData 获取最原始的触摸信息,再对原始触摸信息进行解析处理
原始触摸信息的解析处理需要先应用获取每个触摸点的数据包长度,再拆数据包。原始触摸信息拿到的是一个二进制数组,这个二进制数组里面可能包含多个触摸点的信息,需要根据数据包长度拆分为多个触摸点信息
解析处理就是除了前面两个分别是属于 X 和 Y 之外,后面的数据就根据 GetPointerDeviceProperties 方法获取到的触摸描述信息进行套入
此方式的复杂程度比较高,且拿到的是原始的触摸信息,需要做比较多的处理。即使解析到 X 和 Y 坐标点之后,还需要执行坐标的转换,将其转换为屏幕坐标系
这里拿到的 X 和 Y 坐标点是设备坐标系,这里的设备坐标系不是 GetPointerDeviceRects 函数获取 的 pointerDeviceRect
设备范围坐标系,而是对应 GetPointerDeviceProperties 方法获取到的描述符的逻辑最大值和最小值的坐标范围
其正确计算方法为从 GetPointerDeviceProperties 方法获取到的 X 和 Y 描述信息,分别取 POINTER_DEVICE_PROPERTY 的 logicalMax
作为最大值范围。分别将 X 和 Y 除以 logicalMax
缩放到 [0,1]
范围内,再乘以屏幕尺寸即可转换为屏幕坐标系
这里的 屏幕尺寸 是通过 GetPointerDeviceRects 函数获取 的 displayRect
尺寸
转换为屏幕坐标系之后,就需要再次处理 DPI 和转换为窗口坐标系的才能使用
可以看到方式3相对来说还是比较复杂的,但其优点是可以获取到更多的设备描述信息,获取到输入点的更多信息,如可以计算出触摸宽度对应的物理触摸尺寸面积等信息
对于 WPF 框架来说,自然是选最复杂且功能全强的方法了
在 WPF 框架的对接
了解了一个 Win32 应用与 WM_POINTER 消息的对接方式,咱来看看 WPF 具体是如何做的。了解了对接方式之后,阅读 WPF 源代码的方式可以是通过必须调用的方法的引用,找到整个 WPF 的脉络
在开始之前必须说明的是,本文的大部分代码都是有删减的代码,只保留和本文相关的部分。现在 WPF 是完全开源的,基于最友好的 MIT 协议,可以自己拉下来代码进行二次修改发布,想看完全的代码和调试整个过程可以自己从开源地址拉取整个仓库下来,开源地址是: https://github.com/dotnet/wpf
本文以下部分仅为开启 WM_POINTER 消息时的 WPF 框架对接的逻辑,如对不开启 WM_POINTER 支持时的逻辑感兴趣,还请参阅 WPF 触摸到事件
在 WPF 里面,触摸初始化的故事开始是在 PointerTabletDeviceCollection.cs
里面,调用 GetPointerDevices 方法进行初始化获取设备数量,之后的每个设备都调用 GetPointerDeviceProperties 方法,获取 HID 描述符上报的对应设备属性,有删减的代码如下
获取到设备之后,将其转换放入到 WPF 定义的 PointerTabletDevice 里面,大概的代码如下
在 PointerTabletDeviceInfo 的 TryInitialize 方法,即 if (ptdi.TryInitialize())
这行代码里面,将会调用 GetPointerDeviceProperties 获取设备属性信息,其代码逻辑如下
为什么这里会调用 GetPointerDeviceProperties 两次?第一次只是拿数量,第二次才是真正的拿值
回顾以上代码,可以看到 PointerTabletDeviceInfo 对象是在 PointerTabletDeviceCollection 的 Refresh 方法里面创建的,如以下代码所示
从 GetPointerDevices 获取到的 POINTER_DEVICE_INFO
信息会存放在 PointerTabletDeviceInfo
的 _deviceInfo
字段里面,如下面代码所示
调用 GetPointerDeviceProperties 时,就会将 POINTER_DEVICE_INFO
的 device
字段作为参数传入,从而获取到 POINTER_DEVICE_PROPERTY
结构体列表信息
获取到的 POINTER_DEVICE_PROPERTY
结构体信息和 HID 描述符上报的信息非常对应。结构体的定义代码大概如下
根据 HID 基础知识可以知道,通过 usagePageId
和 usageId
即可了解到此设备属性的具体含义。更多请参阅 HID 标准文档: http://www.usb.org/developers/hidpage/Hut1_12v2.pdf
在 WPF 使用到的 Pointer 的 usagePageId
只有以下枚举所列举的值
在 WPF 使用到的 Pointer 的 usageId
只有以下枚举所列举的值
在 WPF 的古老版本里面,约定了使用 GUID 去获取 StylusPointDescription 里面的额外数据信息。为了与此行为兼容,在 WPF 里面就定义了 HidUsagePage 和 HidUsage 与 GUID 的对应关系,实现代码如下
通过以上的 _hidToGuidMap
的定义关联关系,调用 GetKnownGuid 方法,即可将 POINTER_DEVICE_PROPERTY
描述信息关联到 WPF 框架层的定义
具体的对应逻辑如下
以上的一个小细节点在于对 unit 单位的处理,即 StylusPointPropertyUnit? unit = StylusPointPropertyUnitHelper.FromPointerUnit(prop.unit);
这行代码的实现定义,具体实现如下
这里的单位的作用是什么呢?用于和 POINTER_DEVICE_PROPERTY
的物理值做关联对应关系,比如触摸面积 Width 和 Height 的物理尺寸就是通过大概如下算法计算出来的
通过 resolution 与具体后续收到的触摸点的值进行计算,带上 StylusPointPropertyUnit 单位,这就是触摸设备上报的物理尺寸了
以上 logicalMax
和 logicalMin
在行业内常被称为逻辑值,以上的 physicalMax
和 physicalMin
常被称为物理值
经过以上的处理之后,即可将 GetPointerDeviceProperties 拿到的设备属性列表给转换为 WPF 框架对应的定义属性内容
以上过程有一个细节,那就是 GetPointerDeviceProperties 拿到的设备属性列表的顺序是非常关键的,设备属性列表的顺序和在后续 WM_POINTER 消息拿到的裸数据的顺序是直接对应的
大家可以看到,在开启 Pointer 消息时,触摸模块初始化获取触摸信息是完全通过 Win32 的 WM_POINTER 模块提供的相关方法完成的。这里需要和不开 WM_POINTER 消息的从 COM 获取触摸设备信息区分,和 dotnet 读 WPF 源代码笔记 插入触摸设备的初始化获取设备信息 提供的方法是不相同的
完成上述初始化逻辑之后,接下来看看消息循环收到 WM_POINTER 消息的处理
收到 WM_POINTER 消息时,调用 GetRawPointerDeviceData 获取最原始的触摸信息,再对原始触摸信息进行解析处理
在 WPF 里面,大家都知道,底层的消息循环处理的在 HwndSource.cs
里面定义,输入处理部分如下
以上代码的 _stylus
就是根据不同的配置参数决定是否使用 Pointer 消息处理的 HwndPointerInputProvider 类型,代码如下
在本文这里初始化的是 HwndPointerInputProvider 类型,将会进入到 HwndPointerInputProvider 的 FilterMessage 方法处理输入数据
对于收到 Pointer 的按下移动抬起消息,都会进入到 ProcessMessage 方法
进入之前调用的 GetPointerId(wParam)
代码的 GetPointerId 方法实现如下
当然了,以上代码简单写就和下面代码差不多
在 WM_POINTER 的设计上,将会源源不断通过消息循环发送指针消息,发送的指针消息里面不直接包含具体的数据信息,而是只将 PointerId 当成 wparam 发送。咱从消息循环里面拿到的只有 PointerId 的值,转换方法如上述代码所示
为什么是这样设计的呢?考虑到现在大部分触摸屏的精度都不低,至少比许多很便宜鼠标的高,这就可能导致应用程序完全无法顶得住每次触摸数据过来都通过消息循环怼进来。在 WM_POINTER 的设计上,只是将 PointerId 通过消息循环发送过来,具体的消息体数据需要使用 GetPointerInfo 方法来获取。这么设计有什么优势?这么设计是用来解决应用卡顿的时候,被堆积消息的问题。假定现在有三个触摸消息进来,第一个触摸消息进来就发送了 Win32 消息给到应用,然而应用等待到系统收集到了三个触摸点消息时,才调用 GetPointerInfo 方法。那此时系统触摸模块就可以很开森的知道了应用处于卡顿状态,即第二个和第三个触摸消息到来时,判断第一个消息还没被应用消费,就不再发送 Win32 消息给到应用。当应用调用 GetPointerInfo 方法时,就直接返回第三个点给到应用,跳过中间第一个和第二个触摸点。同时,使用历史点的概念,将第一个点和第二个点和第三个点给到应用,如果此时应用感兴趣的话
利用如上所述机制,即可实现到当触摸设备产生的触摸消息过快时,不会让应用的消息循环过度忙碌,而是可以让应用有机会一次性拿到过去一段时间内的多个触摸点信息。如此可以提升整体系统的性能,减少应用程序忙碌于处理过往的触摸消息
举一个虚拟的例子,让大家更好的理解这套机制的思想。假定咱在制作一个应用,应用有一个功能,就是有一个矩形元素,这个元素可以响应触摸拖动,可以用触摸拖动矩形元素。这个应用编写的有些离谱,每次拖动的做法就是设置新的坐标点为当前触摸点,但是这个过程需要 15 毫秒,因为中间添加了一些有趣且保密(其实我还没编出来)的算法。当应用跑在一个触摸设备上,这个触摸设备在触摸拖动的过程中,每 10 毫秒将产生一次触摸点信息报告给到系统。假定当前的系统的触摸模块是如实的每次收到设备发送过来的触摸点,都通过 Win32 消息发送给到应用,那将会让应用的消费速度慢于消息的生产速度,这就意味着大家可以明显看到拖动矩形元素时具备很大的延迟感。如拖着拖着才发现矩形元素还在后面慢慢挪动,整体的体验比较糟糕。那如果采用现在的这套玩法呢?应用程序从 Win32 消息收到的是 PointerId 信息,再通过 GetPointerInfo 方法获取触摸点信息,此时获取到的触摸点就是最后一个触摸点,对于咱这个应用来说刚刚好,直接就是响应设置矩形元素坐标为最后一个触摸点的对应坐标。如此即可看到矩形元素飞快跳着走,且由于刚好矩形元素拖动过程为 15 毫秒,小于 16 毫秒,意味着大部分情况下大家看到的是矩形元素平滑的移动,即飞快跳着走在人类看来是一个连续移动的过程
期望通过以上的例子可以让大家了解到微软的“良苦”用心
这里需要额外说明的是 PointerId 和 TouchDevice 等的 Id 是不一样的,在下文将会给出详细的描述
在 WPF 这边,如上面代码所示,收到触摸点信息之后,将会进入到 ProcessMessage 方法,只是这个过程中我感觉有一点小锅的是,时间戳拿的是当前系统时间戳 Environment.TickCount 的值,而不是取 Pointer 消息里面的时间戳内容
继续看一下 ProcessMessage 方法的定义和实现
在 ProcessMessage 里面将创建 PointerData 对象,这个 PointerData 类型是一个辅助类,在构造函数里面将调用 GetPointerInfo 方法获取指针点信息
以下是 PointerData 构造函数的简单定义的有删减的代码
通过上述代码可以看到,开始是调用 GetPointerInfo 方法获取指针点信息。在 WPF 的基础事件里面也是支持历史点的,意图和 Pointer 的设计意图差不多,都是为了解决业务端的消费数据速度问题。于是在 WPF 底层也就立刻调用 GetPointerInfoHistory 获取历史点信息
对于 Pointer 消息来说,对触摸和触笔有着不同的数据提供分支,分别是 GetPointerTouchInfo 方法和 GetPointerPenInfo 方法
在 PointerData 构造函数里面,也通过判断 POINTER_INFO
的 pointerType
字段决定调用不同的方法,代码如下
对于 WPF 的 HwndPointerInputProvider 模块来说,只处理 PT_TOUCH 和 PT_PEN 消息,即触摸和触笔消息。对于 Mouse 鼠标和 Touchpad 触摸板来说都不走 Pointer 处理,依然是走原来的 Win32 消息。为什么这么设计呢?因为 WPF 里面没有 Pointer 路由事件,在 WPF 里面分开了 Touch 和 Stylus 和 Mouse 事件。就不需要全部都在 Pointer 模块处理了,依然在原来的消息循环里面处理,既减少 Pointer 模块的工作量,也能减少后续从 Pointer 分发到 Touch 和 Stylus 和 Mouse 事件的工作量。原先的模块看起来也跑得很稳,那就一起都不改了
完成 PointerData 的构造函数之后,继续到 HwndPointerInputProvider 的 ProcessMessage 函数里面,在此函数里面判断是 PT_TOUCH 和 PT_PEN 消息,则进行处理
对于触摸和触笔的处理上,先是执行触摸设备关联。触摸设备关联一个在上层业务的表现就是让当前的指针消息关联上 TouchDevice 的 Id 或 StylusDevice 的 Id 值
关联的方法是通过 GetPointerCursorId 方法先获取 CursorId 的值,再配合对应的输入的 Pointer 的输入设备 POINTER_INFO
的 sourceDevice
字段,即可与初始化过程中创建的设备相关联,实现代码如下
在 WPF 初始化工作里面将输入的 Pointer 的输入设备 POINTER_INFO
的 sourceDevice
当成 deviceId
的概念,即 TabletDevice 的 Id 值。而 cursorId
则是对应 StylusDevice 的 Id 值,其更新代码的核心非常简单,如下面代码
对应的 GetByDeviceId 方法的代码如下
对应的 GetStylusByCursorId 的代码如下
通过以上方式即可通过 PointerId 获取的 cursorId
进而获取到对应 WPF 里面的设备对象,进而拿到 WPF 里面的设备 Id 号。通过上文的描述也可以看到 PointerId 和 TouchDevice 等的 Id 是不一样的,但是之间有关联关系
调用了 UpdateCurrentTabletAndStylus 的一个副作用就是同步更新了 _currentTabletDevice
和 _currentStylusDevice
字段的值,后续逻辑即可直接使用这两个字段而不是传参数
完成关联逻辑之后,即进入 GenerateRawStylusData 方法,这个方法是 WPF 获取 Pointer 具体的消息的核心方法,方法签名如下
此 GenerateRawStylusData 被调用是这么写的
在 GenerateRawStylusData 方法里面,先通过 PointerTabletDevice 取出支持的 Pointer 的设备属性列表的长度,用于和输入点的信息进行匹配。回忆一下,这部分获取逻辑是在上文介绍到对 GetPointerDeviceProperties 函数的调用提到的,且也说明了此函数拿到的设备属性列表的顺序是非常关键的,设备属性列表的顺序和在后续 WM_POINTER 消息拿到的裸数据的顺序是直接对应的
由每个 Pointer 的属性长度配合总共的历史点数量,即可获取到这里面使用到的 rawPointerData
数组的长度。这部分代码相信大家很好就理解了
接着就是核心部分,调用 GetRawPointerDeviceData 获取最原始的触摸信息,再对原始触摸信息进行解析处理
在 Pointer 的设计里面,历史点 historyCount
是包含当前点的,且当前点就是最后一个点。这就是为什么这里只需要传入历史点数量即可,换句话说就是历史点最少包含一个点,那就是当前点
由于 Pointer 获取到的点都是相对于屏幕坐标的,这里需要先偏移一下修改为窗口坐标系,代码如下
这里的 GetOriginOffsetsLogical 的实现逻辑就是取窗口的 0,0 点,看这个点会在屏幕的哪里,从而知道其偏移量。至于添加到 MatrixTransform 矩阵的 TabletToScreen 则在后文的具体转换逻辑会讲到,这里先跳过
获取到相对于窗口的坐标偏移量之后,即可将其叠加给到每个点上,用于将这些点转换为窗口坐标系。但是在此之前还需要将获取到的 rawPointerData
进行加工。这一个步骤仅仅只是在 WPF 有需求,仅仅只是为了兼容 WISP 获取到的裸数据的方式。其相差点在于通过 Pointer 获取到的 rawPointerData
的二进制数据格式里面,没有带上按钮的支持情况的信息,在 WPF 这边需要重新创建一个数组对 rawPointerData
重新排列,确保每个点的数据都加上按钮的信息数据
这部分处理仅只是为了兼容考虑,让后续的 StylusPointCollection 开森而已,咱就跳着看就好了
重新拷贝的过程,还将点的坐标更换成窗口坐标系,即以上的 data[i + StylusPointDescription.RequiredXIndex] -= originOffsetX;
和 data[i + StylusPointDescription.RequiredYIndex] -= originOffsetY;
两个代码
完成获取之后,就将获取到的裸数据给返回了,这就是 GenerateRawStylusData 的内容
在 ProcessMessage 方法里面获取到 GenerateRawStylusData 返回的原始指针信息,即可将其给到 RawStylusInputReport 作为参数,代码如下
将创建的 RawStylusInputReport 更新到当前的设备,作为设备的最新的指针信息
且还加入到 InputManager 的 ProcessInput 里面,进入 WPF 的框架内的消息调度
在进入 InputManager 的 ProcessInput 调度消息之前,先看看 _currentStylusDevice.Update
里面的对原始指针信息的解析实现逻辑
在 _currentStylusDevice.Update
里面的对原始指针信息的解析实现完全是靠 StylusPointCollection 和 StylusPoint 的构造函数实现的
这里的 rsir.GetRawPacketData()
是返回上文提到的 GenerateRawStylusData
方法给出的裸数据的拷贝,代码如下
这里的 GetTabletToElementTransform 包含了一个核心转换,方法代码如下
这里面方法存在重点内容,那就是 PointerTabletDevice 的 TabletToScreen 属性的计算方法。这个矩阵的计算需要用到开始初始化过程的 GetPointerDeviceRects 函数获取 的 displayRect
尺寸,以及 GetPointerDeviceProperties 获取的 X 和 Y 属性描述信息,属性的定义代码如下
可以看到这是一个用于缩放的 Matrix 对象,正是 GetPointerDeviceRects 获取的屏幕尺寸以及 GetPointerDeviceProperties 获取的 X 和 Y 属性描述信息构成的 TabletSize 的比值
回顾一下 _tabletInfo
的 SizeInfo 的创建代码,可以看到 TabletSize 完全是由描述符的尺寸决定,代码如下
如此即可使用 TabletToScreen 属性将收到的基于 Tablet 坐标系的裸指针消息的坐标转换为屏幕坐标,再配合 TransformToDevice 取反即可转换到 WPF 坐标系
在以上代码里面,由于传入 GetTabletToElementTransform 的 relativeTo
参数是 null 的值,将导致 StylusDevice.GetElementTransform(relativeTo)
返回一个单位矩阵,这就意味着在 GetTabletToElementTransform 方法里面的 group.Children.Add(StylusDevice.GetElementTransform(relativeTo));
是多余的,也许后续 WPF 版本这里会被我优化掉
回顾一下 StylusPointCollection 的构造函数参数,有用的参数只有前三个,分别是 rsir.StylusPointDescription
传入描述符信息,以及 rsir.GetRawPacketData()
返回裸指针数据,以及 GetTabletToElementTransform(null)
方法返回转换为 WPF 坐标系的矩阵
那 StylusPointCollection 的最后一个参数,即上述代码传入的 Matrix.Identity
有什么用途?其实在 StylusPointCollection 的设计里面,第三个参数和第四个参数是二选一的,且第三个参数的优先级大于第四个参数。即在 StylusPointCollection 底层会判断第三个参数是否有值,如果没有值才会使用第四个参数
在 StylusPointCollection 构造函数里面将会对裸 Pointer 数据进行处理,现在 GetRawPacketData 拿到的裸 Pointer 数据的 int 数组里面的数据排列内容大概如下
存放的是一个或多个点信息,每个点的信息都是相同的二进制长度,分包非常简单
进入到 StylusPointCollection 的构造函数,看看其代码签名定义
在构造函数里面,先调用 StylusPointDescription 的 GetInputArrayLengthPerPoint 方法,获取每个点的二进制长度,代码如下
获取到了一个点的二进制长度,自然就能算出传入的 rawPacketData
参数包含多少个点的信息
以上代码的 Debug.Assert
就是要确保传入的 rawPacketData
是可以被 lengthPerPoint
即每个点的二进制长度所整除
完成准备工作之后,接下来就可以将 rawPacketData
解出点了,如下面代码所示
以上代码忽略的部分包含了一些细节,如对 Point 的坐标转换,使用 Point p = new Point(rawPacketData[i], rawPacketData[i + 1]);
拿到的点的坐标是属于 Tablet 坐标,需要使用传入的参数转换为 WPF 坐标,如下面代码所示
通过以上的代码就可以看到 StylusPointCollection 构造函数使用了第三个或第四个参数作为变换,如果第三个参数存在则优先使用第三个参数
其他处理的逻辑就是对压感的额外处理,压感作为 StylusPoint 的一个明确参数,需要额外判断处理
如此即可解包 | X 坐标 | Y 坐标 | 压感(可选)| StylusPointDescription 里面的属性列表一一对应 |
里面前三个元素,其中压感是可选的。后续的 StylusPointDescription 里面的属性列表一一对应
部分需要重新创建 data 数组传入到各个 StylusPoint 里面,代码如下
后续对 StylusPoint 获取属性时,即可通过描述信息获取,描述信息获取到值的方式就是取以上代码传入的 data
二进制数组的对应下标的元素,比如触摸点的宽度或高度信息
完成转换为 StylusPointCollection 之后,即可使用 InputManager.UnsecureCurrent.ProcessInput
方法将裸输入信息调度到 WPF 输入管理器
进入到 ProcessInput 里面将会走标准的路由事件机制,通过路由机制触发 Touch 或 Stylus 事件,接下来的逻辑看一下调用堆栈即可,和其他的输入事件逻辑差不多
由于我跑的是 Release 版本的 WPF 导致了有一些函数被内联,如从 HwndPointerInputProvider.ProcessMessage
到 InputManager.ProcessStagingArea
中间就少了 InputManager.ProcessInput
函数,完全的无函数内联的堆栈应该如下
如下面代码是 ProcessInput 函数的代码
进入到 ProcessStagingArea 方法会执行具体的调度逻辑,用上述触摸按下的堆栈作为例子,将会进入到 PointerLogic 的 PostProcessInput 方法里面,由 PostProcessInput 方法调用到 PromoteMainToOther 再到 PromoteMainToTouch 最后到 PromoteMainDownToTouch 方法。只不过中间的几个方法被内联了,直接从堆栈上看就是从 RaiseProcessInputEventHandlers 到 PromoteMainDownToTouch 方法,堆栈如下
核心触发按下的代码就在 PromoteMainDownToTouch 里,其代码大概如下
从上文可以知道,在 HwndPointerInputProvider 的 ProcessMessage 里面调用了 _currentStylusDevice.Update
方法时,就将输入的数据存放到 PointerStylusDevice 里面
后续的逻辑就和 WPF 模拟触摸设备 提到的使用方法差不多,只是数据提供源是从 PointerStylusDevice 提供。如果大家对进入到 InputManager 的后续逻辑感兴趣,可参考 WPF 通过 InputManager 模拟调度触摸事件 提供的方法自己跑一下
更多触摸请看 WPF 触摸相关
原文链接: http://blog.lindexi.com/post/dotnet-%E8%AF%BB-WPF-%E6%BA%90%E4%BB%A3%E7%A0%81%E7%AC%94%E8%AE%B0-%E4%BB%8E-WM_POINTER-%E6%B6%88%E6%81%AF%E5%88%B0-Touch-%E4%BA%8B%E4%BB%B6
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系。