dotnet 讀 WPF 源代碼筆記 從 WM_POINTER 消息到 Touch 事件

来源:https://www.cnblogs.com/lindexi/p/18403860
-Advertisement-
Play Games

簡介 在現代微服務架構中,服務發現(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_INFOptPixelLocationRaw 欄位,即可拿到基於屏幕坐標系的像素點

只需將其轉換為視窗坐標系和處理 DPI 即可使用

此方法的最大缺點在於 ptPixelLocationRaw 欄位拿到的是丟失精度的點,像素為單位。如果在精度稍微高的觸摸屏下,將會有明顯的鋸齒效果

優點在於其獲取特別簡單

方式2:

依然是接收到 WM_POINTER 消息之後,將 wparam 轉換為 pointerId 參數,調用 GetPointerTouchInfo 方法即可獲取到 POINTER_INFO 信息

只是從獲取 POINTER_INFOptPixelLocationRaw 欄位換成 ptHimetricLocationRaw 欄位

使用 ptHimetricLocationRaw 欄位的優勢在於可以獲取不丟失精度的信息,但需要額外調用 GetPointerDeviceRects 函數獲取 displayRectpointerDeviceRect 信息用於轉換坐標點

            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的代碼放在 githubgitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快

先創建一個空文件夾,接著使用命令行 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_PROPERTYlogicalMax 作為最大值範圍。分別將 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_INFOdevice 欄位作為參數傳入,從而獲取到 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 基礎知識可以知道,通過 usagePageIdusageId 即可瞭解到此設備屬性的具體含義。更多請參閱 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 單位,這就是觸摸設備上報的物理尺寸了

以上 logicalMaxlogicalMin 在行業內常被稱為邏輯值,以上的 physicalMaxphysicalMin 常被稱為物理值

經過以上的處理之後,即可將 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_INFOpointerType 欄位決定調用不同的方法,代碼如下

            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_INFOsourceDevice 欄位,即可與初始化過程中創建的設備相關聯,實現代碼如下

            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_INFOsourceDevice 當成 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 數組的長度。這部分代碼相信大

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 數據傳輸的過程首先要建立網路連接 。數據傳輸單元為數據包 DATA PRAGRAM. 電腦數 據網路的互通互聯物理硬體和軟體程式的管理。區域網絡是美國國防部連接不同電腦器設 備的一種方式 。光纜傳輸數據的速度更慢 。海底光纖的架設, 2000 年左右使得全球互聯網 時代惠國惠民。電腦信息技術起 ...
  • Python 速查表中文版 本手冊是 Python cheat sheet 的中文翻譯版。原作者:Arianne Colton and Sean Chen([email protected]) 編譯:ucasFL 目錄 常規 數值類類型 數據結構 函數 控制流 面向對象編程 ...
  • 寫在前面 Spring的核心思想就是容器,當容器refresh的時候,外部看上去風平浪靜,其實內部則是一片驚濤駭浪,汪洋一片。Springboot更是封裝了Spring,遵循約定大於配置,加上自動裝配的機制。很多時候我們只要引用了一個依賴,幾乎是零配置就能完成一個功能的裝配。 由spring提供的、 ...
  • 八,SpringBoot Web 開發訪問靜態資源(附+詳細源碼剖析) @目錄八,SpringBoot Web 開發訪問靜態資源(附+詳細源碼剖析)1. 基本介紹2. 快速入門2.1 準備工作3. 改變靜態資源訪問首碼,定義為我們自己想要的4. 改變Spring Boot當中的預設的靜態資源路徑(實 ...
  • 1.下載 Redis for Windows Redis 官方並沒有提供 Windows 版本的安裝包,但你可以使用 Microsoft 維護的 Windows 版本的 Redis。你可以從以下鏈接下載 Redis for Windows: 2.安裝 Redis 運行安裝程式: 雙擊下載的 .msi ...
  • 在Word中,分節符是一種強大的工具,用於將文檔分成不同的部分,每個部分可以有獨立的頁面設置,如頁邊距、紙張方向、頁眉和頁腳等。正確使用分節符可以極大地提升文檔的組織性和專業性,特別是在長文檔中,需要在不同部分應用不同的樣式時。本文將介紹如何使用一個免費的.NET庫通過C#實現插入或刪除Word分節 ...
  • 關說不練假把式,在上一,二篇中介紹了我心目中的CRUD的樣子 基於之前的理念,我開發了一個命名為PasteTemplate的項目,這個項目呢後續會轉化成項目模板,轉化成項目模板後,後續需要開發新的項目就可以基於這個模板創建,這樣就不要copy一個舊的項目,然後刪刪刪,改改改,重命名等操作了 強迫症, ...
  • ZY樹洞 前言 ZY樹洞是一個基於.NET Core開發的簡單的評論系統,主要用於大家分享自己心中的感悟、經驗、心得、想法等。 好了,不賣關子了,這個項目其實是上班無聊的時候寫的,為什麼要寫這個項目呢?因為我單純的想吐槽一下工作中的不滿而已。 項目介紹 項目很簡單,主要功能就是提供一個簡單的評論系統 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...