簡介 在現代微服務架構中,服務發現(Service Discovery)是一項關鍵功能。它允許微服務動態地找到彼此,而無需依賴硬編碼的地址。以前如果你搜 .NET Service Discovery,大概率會搜到一大堆 Eureka,Consul 等的文章。現在微軟為我們帶來了一個官方的包:Micr ...
本文記錄我讀 WPF 源代碼的筆記,在 WPF 底層是如何從 Win32 的消息迴圈獲取到的 WM_POINTER 消息處理轉換作為 Touch 事件的參數
由於 WPF 觸摸部分會兼顧開啟 Pointer 消息和不開啟 Pointer 消息,為了方便大家理解,本文分為兩個部分。第一個部分是脫離 WPF 框架,聊聊一個 Win32 程式如何從 Win32 的消息迴圈獲取到的 WM_POINTER 消息處理轉換為輸入坐標點,以及在觸摸下獲取觸摸信息。第二部分是 WPF 框架是如何安排上這些處理邏輯,如何和 WPF 框架的進行對接
處理 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
信息用於轉換坐標點
PInvoke.GetPointerDeviceRects(pointerInfo.sourceDevice, &pointerDeviceRect, &displayRect);
// 如果想要獲取比較高精度的觸摸點,可以使用 ptHimetricLocationRaw 欄位
// 由於 ptHimetricLocationRaw 採用的是 pointerDeviceRect 坐標系,需要轉換到屏幕坐標系
// 轉換方法就是先將 ptHimetricLocationRaw 的 X 坐標,壓縮到 [0-1] 範圍內,然後乘以 displayRect 的寬度,再加上 displayRect 的 left 值,即得到了屏幕坐標系的 X 坐標。壓縮到 [0-1] 範圍內的方法就是除以 pointerDeviceRect 的寬度
// 為什麼需要加上 displayRect.left 的值?考慮多屏的情況,屏幕可能是副屏
// Y 坐標同理
var point2D = new Point2D(
pointerInfo.ptHimetricLocationRaw.X / (double) pointerDeviceRect.Width * displayRect.Width +
displayRect.left,
pointerInfo.ptHimetricLocationRaw.Y / (double) pointerDeviceRect.Height * displayRect.Height +
displayRect.top);
// 獲取到的屏幕坐標系的點,需要轉換到 WPF 坐標系
// 轉換過程的兩個重點:
// 1. 底層 ClientToScreen 只支持整數類型,直接轉換會丟失精度。即使是 WPF 封裝的 PointFromScreen 或 PointToScreen 方法也會丟失精度
// 2. 需要進行 DPI 換算,必須要求 DPI 感知
// 先測量視窗與屏幕的偏移量,這裡直接取 0 0 點即可,因為這裡獲取到的是虛擬屏幕坐標系,不需要考慮多屏的情況
var screenTranslate = new Point(0, 0);
PInvoke.ClientToScreen(new HWND(hwnd), ref screenTranslate);
// 獲取當前的 DPI 值
var dpi = VisualTreeHelper.GetDpi(this);
// 先做平移,再做 DPI 換算
point2D = new Point2D(point2D.X - screenTranslate.X, point2D.Y - screenTranslate.Y);
point2D = new Point2D(point2D.X / dpi.DpiScaleX, point2D.Y / dpi.DpiScaleY);
以上方式2的代碼放在 github 和 gitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快
先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行裡面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 322313ee55d0eeaae7148b24ca279e1df087871e
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼。如果依然拉取不到代碼,可以發郵件向我要代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 322313ee55d0eeaae7148b24ca279e1df087871e
獲取代碼之後,進入 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
在 WPF 裡面,觸摸初始化的故事開始是在 PointerTabletDeviceCollection.cs
裡面,調用 GetPointerDevices 方法進行初始化獲取設備數量,之後的每個設備都調用 GetPointerDeviceProperties 方法,獲取 HID 描述符上報的對應設備屬性,有刪減的代碼如下
namespace System.Windows.Input.StylusPointer
{
/// <summary>
/// Maintains a collection of pointer device information for currently installed pointer devices
/// </summary>
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
internal void Refresh()
{
... // 忽略其他代碼
UnsafeNativeMethods.POINTER_DEVICE_INFO[] deviceInfos
= new UnsafeNativeMethods.POINTER_DEVICE_INFO[deviceCount];
IsValid = UnsafeNativeMethods.GetPointerDevices(ref deviceCount, deviceInfos);
... // 忽略其他代碼
}
}
}
獲取到設備之後,將其轉換放入到 WPF 定義的 PointerTabletDevice 裡面,大概的代碼如下
namespace System.Windows.Input.StylusPointer
{
/// <summary>
/// Maintains a collection of pointer device information for currently installed pointer devices
/// </summary>
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
internal void Refresh()
{
... // 忽略其他代碼
UnsafeNativeMethods.POINTER_DEVICE_INFO[] deviceInfos
= new UnsafeNativeMethods.POINTER_DEVICE_INFO[deviceCount];
IsValid = UnsafeNativeMethods.GetPointerDevices(ref deviceCount, deviceInfos);
if (IsValid)
{
foreach (var deviceInfo in deviceInfos)
{
// Old PenIMC code gets this id via a straight cast from COM pointer address
// into an int32. This does a very similar thing semantically using the pointer
// to the tablet from the WM_POINTER stack. While it may have similar issues
// (chopping the upper bits, duplicate ids) we don't use this id internally
// and have never received complaints about this in the WISP stack.
int id = MS.Win32.NativeMethods.IntPtrToInt32(deviceInfo.device);
PointerTabletDeviceInfo ptdi = new PointerTabletDeviceInfo(id, deviceInfo);
// Don't add a device that fails initialization. This means we will try a refresh
// next time around if we receive stylus input and the device is not available.
// <see cref="HwndPointerInputProvider.UpdateCurrentTabletAndStylus">
if (ptdi.TryInitialize())
{
PointerTabletDevice tablet = new PointerTabletDevice(ptdi);
_tabletDeviceMap[tablet.Device] = tablet;
TabletDevices.Add(tablet.TabletDevice);
}
}
}
... // 忽略其他代碼
}
/// <summary>
/// Holds a mapping of TabletDevices from their WM_POINTER device id
/// </summary>
private Dictionary<IntPtr, PointerTabletDevice> _tabletDeviceMap = new Dictionary<IntPtr, PointerTabletDevice>();
}
}
namespace System.Windows.Input
{
/// <summary>
/// Collection of the tablet devices that are available on the machine.
/// </summary>
public class TabletDeviceCollection : ICollection, IEnumerable
{
internal List<TabletDevice> TabletDevices { get; set; } = new List<TabletDevice>();
}
}
在 PointerTabletDeviceInfo 的 TryInitialize 方法,即 if (ptdi.TryInitialize())
這行代碼裡面,將會調用 GetPointerDeviceProperties 獲取設備屬性信息,其代碼邏輯如下
namespace System.Windows.Input.StylusPointer
{
/// <summary>
/// WM_POINTER specific information about a TabletDevice
/// </summary>
internal class PointerTabletDeviceInfo : TabletDeviceInfo
{
internal PointerTabletDeviceInfo(int id, UnsafeNativeMethods.POINTER_DEVICE_INFO deviceInfo)
{
_deviceInfo = deviceInfo;
Id = id;
Name = _deviceInfo.productString;
PlugAndPlayId = _deviceInfo.productString;
}
internal bool TryInitialize()
{
... // 忽略其他代碼
var success = TryInitializeSupportedStylusPointProperties();
... // 忽略其他代碼
return success;
}
private bool TryInitializeSupportedStylusPointProperties()
{
bool success = false;
... // 忽略其他代碼
// Retrieve all properties from the WM_POINTER stack
success = UnsafeNativeMethods.GetPointerDeviceProperties(Device, ref propCount, null);
if (success)
{
success = UnsafeNativeMethods.GetPointerDeviceProperties(Device, ref propCount, SupportedPointerProperties);
if (success)
{
... // 執行更具體的初始化邏輯
}
}
... // 忽略其他代碼
}
/// <summary>
/// The specific id for this TabletDevice
/// </summary>
internal IntPtr Device { get { return _deviceInfo.device; } }
/// <summary>
/// Store the WM_POINTER device information directly
/// </summary>
private UnsafeNativeMethods.POINTER_DEVICE_INFO _deviceInfo;
}
}
為什麼這裡會調用 GetPointerDeviceProperties 兩次?第一次只是拿數量,第二次才是真正的拿值
回顧以上代碼,可以看到 PointerTabletDeviceInfo 對象是在 PointerTabletDeviceCollection 的 Refresh 方法裡面創建的,如以下代碼所示
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
internal void Refresh()
{
... // 忽略其他代碼
UnsafeNativeMethods.POINTER_DEVICE_INFO[] deviceInfos
= new UnsafeNativeMethods.POINTER_DEVICE_INFO[deviceCount];
IsValid = UnsafeNativeMethods.GetPointerDevices(ref deviceCount, deviceInfos);
foreach (var deviceInfo in deviceInfos)
{
// Old PenIMC code gets this id via a straight cast from COM pointer address
// into an int32. This does a very similar thing semantically using the pointer
// to the tablet from the WM_POINTER stack. While it may have similar issues
// (chopping the upper bits, duplicate ids) we don't use this id internally
// and have never received complaints about this in the WISP stack.
int id = MS.Win32.NativeMethods.IntPtrToInt32(deviceInfo.device);
PointerTabletDeviceInfo ptdi = new PointerTabletDeviceInfo(id, deviceInfo);
if (ptdi.TryInitialize())
{
}
}
... // 忽略其他代碼
}
}
從 GetPointerDevices 獲取到的 POINTER_DEVICE_INFO
信息會存放在 PointerTabletDeviceInfo
的 _deviceInfo
欄位裡面,如下麵代碼所示
internal class PointerTabletDeviceInfo : TabletDeviceInfo
{
internal PointerTabletDeviceInfo(int id, UnsafeNativeMethods.POINTER_DEVICE_INFO deviceInfo)
{
_deviceInfo = deviceInfo;
Id = id;
}
/// <summary>
/// The specific id for this TabletDevice
/// </summary>
internal IntPtr Device { get { return _deviceInfo.device; } }
/// <summary>
/// Store the WM_POINTER device information directly
/// </summary>
private UnsafeNativeMethods.POINTER_DEVICE_INFO _deviceInfo;
}
調用 GetPointerDeviceProperties 時,就會將 POINTER_DEVICE_INFO
的 device
欄位作為參數傳入,從而獲取到 POINTER_DEVICE_PROPERTY
結構體列表信息
獲取到的 POINTER_DEVICE_PROPERTY
結構體信息和 HID 描述符上報的信息非常對應。結構體的定義代碼大概如下
/// <summary>
/// A struct representing the information for a particular pointer property.
/// These correspond to the raw data from WM_POINTER.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct POINTER_DEVICE_PROPERTY
{
internal Int32 logicalMin;
internal Int32 logicalMax;
internal Int32 physicalMin;
internal Int32 physicalMax;
internal UInt32 unit;
internal UInt32 unitExponent;
internal UInt16 usagePageId;
internal UInt16 usageId;
}
根據 HID 基礎知識可以知道,通過 usagePageId
和 usageId
即可瞭解到此設備屬性的具體含義。更多請參閱 HID 標準文檔: http://www.usb.org/developers/hidpage/Hut1_12v2.pdf
在 WPF 使用到的 Pointer 的 usagePageId
的只有以下枚舉所列舉的值
/// <summary>
///
/// WM_POINTER stack must parse out HID spec usage pages
/// <see cref="http://www.usb.org/developers/hidpage/Hut1_12v2.pdf"/>
/// </summary>
internal enum HidUsagePage
{
Undefined = 0x00,
Generic = 0x01,
Simulation = 0x02,
Vr = 0x03,
Sport = 0x04,
Game = 0x05,
Keyboard = 0x07,
Led = 0x08,
Button = 0x09,
Ordinal = 0x0a,
Telephony = 0x0b,
Consumer = 0x0c,
Digitizer = 0x0d,
Unicode = 0x10,
Alphanumeric = 0x14,
BarcodeScanner = 0x8C,
WeighingDevice = 0x8D,
MagneticStripeReader = 0x8E,
CameraControl = 0x90,
MicrosoftBluetoothHandsfree = 0xfff3,
}
在 WPF 使用到的 Pointer 的 usageId
的只有以下枚舉所列舉的值
/// <summary>
///
///
/// WISP pre-parsed these, WM_POINTER stack must do it itself
///
/// See Stylus\biblio.txt - 1
/// <see cref="http://www.usb.org/developers/hidpage/Hut1_12v2.pdf"/>
/// </summary>
internal enum HidUsage
{
TipPressure = 0x30,
X = 0x30,
BarrelPressure = 0x31,
Y = 0x31,
Z = 0x32,
XTilt = 0x3D,
YTilt = 0x3E,
Azimuth = 0x3F,
Altitude = 0x40,
Twist = 0x41,
TipSwitch = 0x42,
SecondaryTipSwitch = 0x43,
BarrelSwitch = 0x44,
TouchConfidence = 0x47,
Width = 0x48,
Height = 0x49,
TransducerSerialNumber = 0x5B,
}
在 WPF 的古老版本裡面,約定了使用 GUID 去獲取 StylusPointDescription 裡面的額外數據信息。為了與此行為相容,在 WPF 裡面就定義了 HidUsagePage 和 HidUsage 與 GUID 的對應關係,實現代碼如下
namespace System.Windows.Input
{
/// <summary>
/// StylusPointPropertyIds
/// </summary>
/// <ExternalAPI/>
internal static class StylusPointPropertyIds
{
/// <summary>
/// The x-coordinate in the tablet coordinate space.
/// </summary>
/// <ExternalAPI/>
public static readonly Guid X = new Guid(0x598A6A8F, 0x52C0, 0x4BA0, 0x93, 0xAF, 0xAF, 0x35, 0x74, 0x11, 0xA5, 0x61);
/// <summary>
/// The y-coordinate in the tablet coordinate space.
/// </summary>
/// <ExternalAPI/>
public static readonly Guid Y = new Guid(0xB53F9F75, 0x04E0, 0x4498, 0xA7, 0xEE, 0xC3, 0x0D, 0xBB, 0x5A, 0x90, 0x11);
public static readonly Guid Z = ...
...
/// <summary>
///
/// WM_POINTER stack usage preparation based on associations maintained from the legacy WISP based stack
/// </summary>
private static Dictionary<HidUsagePage, Dictionary<HidUsage, Guid>> _hidToGuidMap = new Dictionary<HidUsagePage, Dictionary<HidUsage, Guid>>()
{
{ HidUsagePage.Generic,
new Dictionary<HidUsage, Guid>()
{
{ HidUsage.X, X },
{ HidUsage.Y, Y },
{ HidUsage.Z, Z },
}
},
{ HidUsagePage.Digitizer,
new Dictionary<HidUsage, Guid>()
{
{ HidUsage.Width, Width },
{ HidUsage.Height, Height },
{ HidUsage.TouchConfidence, SystemTouch },
{ HidUsage.TipPressure, NormalPressure },
{ HidUsage.BarrelPressure, ButtonPressure },
{ HidUsage.XTilt, XTiltOrientation },
{ HidUsage.YTilt, YTiltOrientation },
{ HidUsage.Azimuth, AzimuthOrientation },
{ HidUsage.Altitude, AltitudeOrientation },
{ HidUsage.Twist, TwistOrientation },
{ HidUsage.TipSwitch, TipButton },
{ HidUsage.SecondaryTipSwitch, SecondaryTipButton },
{ HidUsage.BarrelSwitch, BarrelButton },
{ HidUsage.TransducerSerialNumber, SerialNumber },
}
},
};
/// <summary>
/// Retrieves the GUID of the stylus property associated with the usage page and usage ids
/// within the HID specification.
/// </summary>
/// <param name="page">The usage page id of the HID specification</param>
/// <param name="usage">The usage id of the HID specification</param>
/// <returns>
/// If known, the GUID associated with the usagePageId and usageId.
/// If not known, GUID.Empty
/// </returns>
internal static Guid GetKnownGuid(HidUsagePage page, HidUsage usage)
{
Guid result = Guid.Empty;
Dictionary<HidUsage, Guid> pageMap = null;
if (_hidToGuidMap.TryGetValue(page, out pageMap))
{
pageMap.TryGetValue(usage, out result);
}
return result;
}
}
}
通過以上的 _hidToGuidMap
的定義關聯關係,調用 GetKnownGuid 方法,即可將 POINTER_DEVICE_PROPERTY
描述信息關聯到 WPF 框架層的定義
具體的對應邏輯如下
namespace System.Windows.Input.StylusPointer
{
/// <summary>
/// Contains a WM_POINTER specific functions to parse out stylus property info
/// </summary>
internal class PointerStylusPointPropertyInfoHelper
{
/// <summary>
/// Creates WPF property infos from WM_POINTER device properties. This appropriately maps and converts HID spec
/// properties found in WM_POINTER to their WPF equivalents. This is based on code from the WISP implementation
/// that feeds the legacy WISP based stack.
/// </summary>
/// <param name="prop">The pointer property to convert</param>
/// <returns>The equivalent WPF property info</returns>
internal static StylusPointPropertyInfo CreatePropertyInfo(UnsafeNativeMethods.POINTER_DEVICE_PROPERTY prop)
{
StylusPointPropertyInfo result = null;
// Get the mapped GUID for the HID usages
Guid propGuid =
StylusPointPropertyIds.GetKnownGuid(
(StylusPointPropertyIds.HidUsagePage)prop.usagePageId,
(StylusPointPropertyIds.HidUsage)prop.usageId);
if (propGuid != Guid.Empty)
{
StylusPointProperty stylusProp = new StylusPointProperty(propGuid, StylusPointPropertyIds.IsKnownButton(propGuid));
// Set Units
StylusPointPropertyUnit? unit = StylusPointPropertyUnitHelper.FromPointerUnit(prop.unit);
// If the parsed unit is invalid, set the default
if (!unit.HasValue)
{
unit = StylusPointPropertyInfoDefaults.GetStylusPointPropertyInfoDefault(stylusProp).Unit;
}
// Set to default resolution
float resolution = StylusPointPropertyInfoDefaults.GetStylusPointPropertyInfoDefault(stylusProp).Resolution;
short mappedExponent = 0;
if (_hidExponentMap.TryGetValue((byte)(prop.unitExponent & HidExponentMask), out mappedExponent))
{
float exponent = (float)Math.Pow(10, mappedExponent);
// Guard against divide by zero or negative resolution
if (prop.physicalMax - prop.physicalMin > 0)
{
// Calculated resolution is a scaling factor from logical units into the physical space
// at the given exponentiation.
resolution =
(prop.logicalMax - prop.logicalMin) / ((prop.physicalMax - prop.physicalMin) * exponent);
}
}
result = new StylusPointPropertyInfo(
stylusProp,
prop.logicalMin,
prop.logicalMax,
unit.Value,
resolution);
}
return result;
}
}
}
以上的一個小細節點在於對 unit 單位的處理,即 StylusPointPropertyUnit? unit = StylusPointPropertyUnitHelper.FromPointerUnit(prop.unit);
這行代碼的實現定義,具體實現如下
internal static class StylusPointPropertyUnitHelper
{
/// <summary>
/// Convert WM_POINTER units to WPF units
/// </summary>
/// <param name="pointerUnit"></param>
/// <returns></returns>
internal static StylusPointPropertyUnit? FromPointerUnit(uint pointerUnit)
{
StylusPointPropertyUnit unit = StylusPointPropertyUnit.None;
_pointerUnitMap.TryGetValue(pointerUnit & UNIT_MASK, out unit);
return (IsDefined(unit)) ? unit : (StylusPointPropertyUnit?)null;
}
/// <summary>
/// Mapping for WM_POINTER based unit, taken from legacy WISP code
/// </summary>
private static Dictionary<uint, StylusPointPropertyUnit> _pointerUnitMap = new Dictionary<uint, StylusPointPropertyUnit>()
{
{ 1, StylusPointPropertyUnit.Centimeters },
{ 2, StylusPointPropertyUnit.Radians },
{ 3, StylusPointPropertyUnit.Inches },
{ 4, StylusPointPropertyUnit.Degrees },
};
/// <summary>
/// Mask to extract units from raw WM_POINTER data
/// <see cref="http://www.usb.org/developers/hidpage/Hut1_12v2.pdf"/>
/// </summary>
private const uint UNIT_MASK = 0x000F;
}
這裡的單位的作用是什麼呢?用於和 POINTER_DEVICE_PROPERTY
的物理值做關聯對應關係,比如觸摸面積 Width 和 Height 的物理尺寸就是通過大概如下演算法計算出來的
short mappedExponent = 0;
if (_hidExponentMap.TryGetValue((byte)(prop.unitExponent & HidExponentMask), out mappedExponent))
{
float exponent = (float)Math.Pow(10, mappedExponent);
// Guard against divide by zero or negative resolution
if (prop.physicalMax - prop.physicalMin > 0)
{
// Calculated resolution is a scaling factor from logical units into the physical space
// at the given exponentiation.
resolution =
(prop.logicalMax - prop.logicalMin) / ((prop.physicalMax - prop.physicalMin) * exponent);
}
}
/// <summary>
/// Contains the mappings from WM_POINTER exponents to our local supported values.
/// This mapping is taken from WISP code, see Stylus\Biblio.txt - 4,
/// as an array of HidExponents.
/// </summary>
private static Dictionary<byte, short> _hidExponentMap = new Dictionary<byte, short>()
{
{ 5, 5 },
{ 6, 6 },
{ 7, 7 },
{ 8, -8 },
{ 9, -7 },
{ 0xA, -6 },
{ 0xB, -5 },
{ 0xC, -4 },
{ 0xD, -3 },
{ 0xE, -2 },
{ 0xF, -1 },
};
通過 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
裡面定義,輸入處理部分如下
namespace System.Windows.Interop
{
/// <summary>
/// The HwndSource class presents content within a Win32 HWND.
/// </summary>
public class HwndSource : PresentationSource, IDisposable, IWin32Window, IKeyboardInputSink
{
private IntPtr InputFilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
... // 忽略其他代碼
// NOTE (alexz): invoke _stylus.FilterMessage before _mouse.FilterMessage
// to give _stylus a chance to eat mouse message generated by stylus
if (!_isDisposed && _stylus != null && !handled)
{
result = _stylus.Value.FilterMessage(hwnd, message, wParam, lParam, ref handled);
}
... // 忽略其他代碼
}
private SecurityCriticalDataClass<IStylusInputProvider> _stylus;
}
}
以上代碼的 _stylus
就是根據不同的配置參數決定是否使用 Pointer 消息處理的 HwndPointerInputProvider 類型,代碼如下
namespace System.Windows.Interop
{
/// <summary>
/// The HwndSource class presents content within a Win32 HWND.
/// </summary>
public class HwndSource : PresentationSource, IDisposable, IWin32Window, IKeyboardInputSink
{
private void Initialize(HwndSourceParameters parameters)
{
... // 忽略其他代碼
if (StylusLogic.IsStylusAndTouchSupportEnabled)
{
// Choose between Wisp and Pointer stacks
if (StylusLogic.IsPointerStackEnabled)
{
_stylus = new SecurityCriticalDataClass<IStylusInputProvider>(new HwndPointerInputProvider(this));
}
else
{
_stylus = new SecurityCriticalDataClass<IStylusInputProvider>(new HwndStylusInputProvider(this));
}
}
... // 忽略其他代碼
}
}
}
在本文這裡初始化的是 HwndPointerInputProvider 類型,將會進入到 HwndPointerInputProvider 的 FilterMessage 方法處理輸入數據
namespace System.Windows.Interop
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Processes the message loop for the HwndSource, filtering WM_POINTER messages where needed
/// </summary>
/// <param name="hwnd">The hwnd the message is for</param>
/// <param name="msg">The message</param>
/// <param name="wParam"></param>
/// <param name="lParam"></param>
/// <param name="handled">If this has been successfully processed</param>
/// <returns></returns>
IntPtr IStylusInputProvider.FilterMessage(IntPtr hwnd, WindowMessage msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
handled = false;
// Do not process any messages if the stack was disabled via reflection hack
if (PointerLogic.IsEnabled)
{
switch (msg)
{
case WindowMessage.WM_ENABLE:
{
IsWindowEnabled = MS.Win32.NativeMethods.IntPtrToInt32(wParam) == 1;
}
break;
case WindowMessage.WM_POINTERENTER:
{
// Enter can be processed as an InRange.
// The MSDN documentation is not correct for InRange (according to feisu)
// As such, using enter is the correct way to generate this. This is also what DirectInk uses.
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.InRange, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERUPDATE:
{
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.Move, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERDOWN:
{
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.Down, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERUP:
{
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.Up, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERLEAVE:
{
// Leave can be processed as an OutOfRange.
// The MSDN documentation is not correct for OutOfRange (according to feisu)
// As such, using leave is the correct way to generate this. This is also what DirectInk uses.
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.OutOfRange, Environment.TickCount);
}
break;
}
}
return IntPtr.Zero;
}
... // 忽略其他代碼
}
}
對於收到 Pointer 的按下移動抬起消息,都會進入到 ProcessMessage 方法
進入之前調用的 GetPointerId(wParam)
代碼的 GetPointerId 方法實現如下
/// <summary>
/// Extracts the pointer id
/// </summary>
/// <param name="wParam">The parameter containing the id</param>
/// <returns>The pointer id</returns>
private uint GetPointerId(IntPtr wParam)
{
return (uint)MS.Win32.NativeMethods.SignedLOWORD(wParam);
}
internal partial class NativeMethods
{
public static int SignedLOWORD(IntPtr intPtr)
{
return SignedLOWORD(IntPtrToInt32(intPtr));
}
public static int IntPtrToInt32(IntPtr intPtr)
{
return unchecked((int)intPtr.ToInt64());
}
public static int SignedLOWORD(int n)
{
int i = (int)(short)(n & 0xFFFF);
return i;
}
}
當然了,以上代碼簡單寫就和下麵代碼差不多
var pointerId = (uint) (ToInt32(wparam) & 0xFFFF);
在 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 方法的定義和實現
namespace System.Windows.Interop
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Processes the latest WM_POINTER message and forwards it to the WPF input stack.
/// </summary>
/// <param name="pointerId">The id of the pointer message</param>
/// <param name="action">The stylus action being done</param>
/// <param name="timestamp">The time (in ticks) the message arrived</param>
/// <returns>True if successfully processed (handled), false otherwise</returns>
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
... // 忽略其他代碼
}
}
... // 忽略其他代碼
}
在 ProcessMessage 裡面將創建 PointerData 對象,這個 PointerData 類型是一個輔助類,在構造函數裡面將調用 GetPointerInfo 方法獲取指針點信息
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
bool handled = false;
// Acquire all pointer data needed
PointerData data = new PointerData(pointerId);
... // 忽略其他代碼
}
以下是 PointerData 構造函數的簡單定義的有刪減的代碼
namespace System.Windows.Input.StylusPointer
{
/// <summary>
/// Provides a wrapping class that aggregates Pointer data from a pointer event/message
/// </summary>
internal class PointerData
{
/// <summary>
/// Queries all needed data from a particular pointer message and stores
/// it locally.
/// </summary>
/// <param name="pointerId">The id of the pointer message</param>
internal PointerData(uint pointerId)
{
if (IsValid = GetPointerInfo(pointerId, ref _info))
{
_history = new POINTER_INFO[_info.historyCount];
// Fill the pointer history
// If we fail just return a blank history
if (!GetPointerInfoHistory(pointerId, ref _info.historyCount, _history))
{
_history = Array.Empty<POINTER_INFO>();
}
... // 忽略其他代碼
}
}
/// <summary>
/// Standard pointer information
/// </summary>
private POINTER_INFO _info;
/// <summary>
/// The full history available for the current pointer (used for coalesced input)
/// </summary>
private POINTER_INFO[] _history;
/// <summary>
/// If true, we have correctly queried pointer data, false otherwise.
/// </summary>
internal bool IsValid { get; private set; } = false;
}
通過上述代碼可以看到,開始是調用 GetPointerInfo 方法獲取指針點信息。在 WPF 的基礎事件裡面也是支持歷史點的,意圖和 Pointer 的設計意圖差不多,都是為瞭解決業務端的消費數據速度問題。於是在 WPF 底層也就立刻調用 GetPointerInfoHistory 獲取歷史點信息
對於 Pointer 消息來說,對觸摸和觸筆有著不同的數據提供分支,分別是 GetPointerTouchInfo 方法和 GetPointerPenInfo 方法
在 PointerData 構造函數裡面,也通過判斷 POINTER_INFO
的 pointerType
欄位決定調用不同的方法,代碼如下
if (IsValid = GetPointerInfo(pointerId, ref _info))
{
switch (_info.pointerType)
{
case POINTER_INPUT_TYPE.PT_TOUCH:
{
// If we have a touch device, pull the touch specific information down
IsValid &= GetPointerTouchInfo(pointerId, ref _touchInfo);
}
break;
case POINTER_INPUT_TYPE.PT_PEN:
{
// Otherwise we have a pen device, so pull down pen specific information
IsValid &= GetPointerPenInfo(pointerId, ref _penInfo);
}
break;
default:
{
// Only process touch or pen messages, do not process mouse or touchpad
IsValid = false;
}
break;
}
}
對於 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 消息,則進行處理
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
bool handled = false;
// Acquire all pointer data needed
PointerData data = new PointerData(pointerId);
// Only process touch or pen messages, do not process mouse or touchpad
if (data.IsValid
&& (data.Info.pointerType == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_TOUCH
|| data.Info.pointerType == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_PEN))
{
... // 忽略其他代碼
}
return handled;
}
對於觸摸和觸筆的處理上,先是執行觸摸設備關聯。觸摸設備關聯一個在上層業務的表現就是讓當前的指針消息關聯上 TouchDevice 的 Id 或 StylusDevice 的 Id 值
關聯的方法是通過 GetPointerCursorId 方法先獲取 CursorId 的值,再配合對應的輸入的 Pointer 的輸入設備 POINTER_INFO
的 sourceDevice
欄位,即可與初始化過程中創建的設備相關聯,實現代碼如下
if (data.IsValid
&& (data.Info.pointerType == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_TOUCH
|| data.Info.pointerType == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_PEN))
{
uint cursorId = 0;
if (UnsafeNativeMethods.GetPointerCursorId(pointerId, ref cursorId))
{
IntPtr deviceId = data.Info.sourceDevice;
// If we cannot acquire the latest tablet and stylus then wait for the
// next message.
if (!UpdateCurrentTabletAndStylus(deviceId, cursorId))
{
return false;
}
... // 忽略其他代碼
}
... // 忽略其他代碼
}
在 WPF 初始化工作裡面將輸入的 Pointer 的輸入設備 POINTER_INFO
的 sourceDevice
當成 deviceId
的概念,即 TabletDevice 的 Id 值。而 cursorId
則是對應 StylusDevice 的 Id 值,其更新代碼的核心非常簡單,如下麵代碼
/// <summary>
/// Attempts to update the current stylus and tablet devices for the latest WM_POINTER message.
/// Will attempt retries if the tablet collection is invalid or does not contain the proper ids.
/// </summary>
/// <param name="deviceId">The id of the TabletDevice</param>
/// <param name="cursorId">The id of the StylusDevice</param>
/// <returns>True if successfully updated, false otherwise.</returns>
private bool UpdateCurrentTabletAndStylus(IntPtr deviceId, uint cursorId)
{
_currentTabletDevice = tablets?.GetByDeviceId(deviceId);
_currentStylusDevice = _currentTabletDevice?.GetStylusByCursorId(cursorId);
... // 忽略其他代碼
if (_currentTabletDevice == null || _currentStylusDevice == null)
{
return false;
}
return true;
}
對應的 GetByDeviceId 方法的代碼如下
namespace System.Windows.Input.StylusPointer
{
/// <summary>
/// Maintains a collection of pointer device information for currently installed pointer devices
/// </summary>
internal class PointerTabletDeviceCollection : TabletDeviceCollection
{
/// <summary>
/// Holds a mapping of TabletDevices from their WM_POINTER device id
/// </summary>
private Dictionary<IntPtr, PointerTabletDevice> _tabletDeviceMap = new Dictionary<IntPtr, PointerTabletDevice>();
... // 忽略其他代碼
/// <summary>
/// Retrieve the TabletDevice associated with the device id
/// </summary>
/// <param name="deviceId">The device id</param>
/// <returns>The TabletDevice associated with the device id</returns>
internal PointerTabletDevice GetByDeviceId(IntPtr deviceId)
{
PointerTabletDevice tablet = null;
_tabletDeviceMap.TryGetValue(deviceId, out tablet);
return tablet;
}
}
}
對應的 GetStylusByCursorId 的代碼如下
namespace System.Windows.Input.StylusPointer
{
/// <summary>
/// A WM_POINTER based implementation of the TabletDeviceBase class.
/// </summary>
internal class PointerTabletDevice : TabletDeviceBase
{
/// <summary>
/// A mapping from StylusDevice id to the actual StylusDevice for quick lookup.
/// </summary>
private Dictionary<uint, PointerStylusDevice> _stylusDeviceMap = new Dictionary<uint, PointerStylusDevice>();
/// <summary>
/// Retrieves the StylusDevice associated with the cursor id.
/// </summary>
/// <param name="cursorId">The id of the StylusDevice to retrieve</param>
/// <returns>The StylusDevice associated with the id</returns>
internal PointerStylusDevice GetStylusByCursorId(uint cursorId)
{
PointerStylusDevice stylus = null;
_stylusDeviceMap.TryGetValue(cursorId, out stylus);
return stylus;
}
}
}
調用了 UpdateCurrentTabletAndStylus 的一個副作用就是同步更新了 _currentTabletDevice
和 _currentStylusDevice
欄位的值,後續邏輯即可直接使用這兩個欄位而不是傳參數
完成關聯邏輯之後,即進入 GenerateRawStylusData 方法,這個方法是 WPF 獲取 Pointer 具體的消息的核心方法,方法簽名如下
namespace System.Windows.Interop
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Creates raw stylus data from the raw WM_POINTER properties
/// </summary>
/// <param name="pointerData">The current pointer info</param>
/// <param name="tabletDevice">The current TabletDevice</param>
/// <returns>An array of raw pointer data</returns>
private int[] GenerateRawStylusData(PointerData pointerData, PointerTabletDevice tabletDevice)
{
... // 忽略其他代碼
}
... // 忽略其他代碼
}
}
此 GenerateRawStylusData 被調用是這麼寫的
namespace System.Windows.Interop
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Processes the latest WM_POINTER message and forwards it to the WPF input stack.
/// </summary>
/// <param name="pointerId">The id of the pointer message</param>
/// <param name="action">The stylus action being done</param>
/// <param name="timestamp">The time (in ticks) the message arrived</param>
/// <returns>True if successfully processed (handled), false otherwise</returns>
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
PointerData data = new PointerData(pointerId);
... // 忽略其他代碼
uint cursorId = 0;
if (UnsafeNativeMethods.GetPointerCursorId(pointerId, ref cursorId))
{
... // 忽略其他代碼
GenerateRawStylusData(data, _currentTabletDevice);
... // 忽略其他代碼
}
}
... // 忽略其他代碼
}
}
在 GenerateRawStylusData 方法裡面,先通過 PointerTabletDevice 取出支持的 Pointer 的設備屬性列表的長度,用於和輸入點的信息進行匹配。回憶一下,這部分獲取邏輯是在上文介紹到對 GetPointerDeviceProperties 函數的調用提到的,且也說明瞭此函數拿到的設備屬性列表的順序是非常關鍵的,設備屬性列表的順序和在後續 WM_POINTER 消息拿到的裸數據的順序是直接對應的
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
/// <summary>
/// Creates raw stylus data from the raw WM_POINTER properties
/// </summary>
/// <param name="pointerData">The current pointer info</param>
/// <param name="tabletDevice">The current TabletDevice</param>
/// <returns>An array of raw pointer data</returns>
private int[] GenerateRawStylusData(PointerData pointerData, PointerTabletDevice tabletDevice)
{
// Since we are copying raw pointer data, we want to use every property supported by this pointer.
// We may never access some of the unknown (unsupported by WPF) properties, but they should be there
// for consumption by the developer.
int pointerPropertyCount = tabletDevice.DeviceInfo.SupportedPointerProperties.Length;
// The data is as wide as the pointer properties and is per history point
int[] rawPointerData = new int[pointerPropertyCount * pointerData.Info.historyCount];
... // 忽略其他代碼
}
... // 忽略其他代碼
}
由每個 Pointer 的屬性長度配合總共的歷史點數量,即可獲取到這裡面使用到的 rawPointerData
數組的長度。這部分代碼相信大