探索 WPF 的 ITabletManager.GetTabletCount 在 Win11 系統的底層實現

本文將和大家介紹專爲 WPF 觸摸模塊提供的 ITabletManager 的 GetTabletCount 方法在 Windows 11 系統的底層實現

本文屬於 WPF 觸摸相關係列博客,偏系統底層介紹,更多觸摸博客請看 WPF 觸摸相關

大家都知道在 Windows 7 系統,有專門的筆和觸摸服務提供觸摸消息的支持。而 WPF 是從 Vista 年代就開始的框架,自然需要支持到 XP 系統。在 XP 系統裏面,還沒有完善的 WM_Touch 消息,同時又需要兼顧性能,最好走的是 RealTimeStylus 這一套。在 Windows 下有一套專門給 WPF 觸摸模塊使用 COM 接口,這一套接口提供了和 RealTimeStylus 幾乎一樣的實現功能,詳細請看 https://learn.microsoft.com/en-us/windows/win32/tablet/com-apis-used-by-windows-presentation-foundation

但是從 Win10 開始,系統裏面就沒有了專門的筆和觸摸服務,而是將觸摸消息集成到系統裏面

本文就來和大家聊聊在 Windows 11 下的 WPF 的觸摸底層,也就是 ITabletManager 接口是定義在哪裏,以及裏面的 GetTabletCount 方法是如何實現

由於各個系統都可以對此進行更改,本文着重在於編寫調試用的代碼,在 VisualStudio 和 IDA 的輔助下了解在 Windows 11 22H2 22621 上的實現

爲了瞭解 ITabletManager 的具體實現 DLL 在哪,可以定義出 COM 接口,通過拿到 COM 接口的虛函數表地址從而瞭解到對應的 DLL 文件

先編寫定義 ITabletManager 接口的代碼,代碼如下

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HRESULT = System.Int32;

[ComImport, Guid("764DE8AA-1867-47C1-8F6A-122445ABD89A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITabletManager
{
    int GetDefaultTablet(out ITablet ppTablet);
    int GetTabletCount(out ulong pcTablets);
    int GetTablet(ulong iTablet, out ITablet ppTablet);
}

以上的 ITablet 接口不是本文的重點,咱只需要定義空接口即可,不需要定義裏面的方法

[ComImport, Guid("1CB2EFC3-ABC7-4172-8FCB-3BC9CB93E29F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITablet //: IUnknown
{
}

接着在代碼裏面,通過如文檔所述方法,先創建 CLSID_TabletManagerS 對象,再將其轉換爲 ITabletManager 接口

Call CoCreateInstance with a class ID of CLSID_TabletManagerS, and then call QueryInterface to get a pointer to the ITabletManager Interface. The CLSID_TabletManagerS GUID is defined as follows: #define CLSID_TabletManagerS uuid(A5B020FD-E04B-4e67-B65A-E7DEED25B2CF)

以上文檔對應的 C# 代碼如下

            var typeFromClsid = Type.GetTypeFromCLSID(new Guid("A5B020FD-E04B-4e67-B65A-E7DEED25B2CF"));
            object comObject = Activator.CreateInstance(typeFromClsid);

            var manager = comObject as ITabletManager;
            manager!.GetTabletCount(out var tabletCount);

開啓本機調試,運行代碼,在以上的代碼的最後一句話下斷點,進入斷點之後即可展開 comObject 的本機視圖,找到 COM 對象的 __vfptr 地址。再根據地址從 VisualStudio 的調試模塊裏面找到落在其中的地址範圍內的 DLL 文件。如下圖

在寫到這裏我纔看到 VisualStudio 裏已經寫了 wisp.dll 文件了,不需要自己去算地址,也是方便哈

瞭解到了現在的 ITabletManager 是定義在 C:\Windows\System32\wisp.dll 文件,即可將此文件丟到 IDA 裏面反編譯一下,如下圖

可以看到在第 53 行裏使用的是 GetPointerDevices 方法。我感覺這就是核心實現了,這個 GetPointerDevices 是在 Win10 下的 WM_Pointer 觸摸系列下的獲取觸摸設備數量的方法

也就是說 ITabletManager 的 GetTabletCount 的核心實現又到 POINTER 機制裏面了。這就超過了本文的範圍了哈,不過能夠知道 ITabletManager 的 GetTabletCount 底層也是到 POINTER 機制也就足夠我玩的。因爲這側面反映了 Win11 不是保留舊代碼,而是 API 重定向和加上兼容的代碼而已。換句話說,如果有一個 bug 是 Pointer 層存在的,那麼 WPF 的 COM 觸摸層也會存在。但反過來不成立,如果有某個是 bug 是在 WPF 的 COM 觸摸層存在的,可能是因爲 Win11 的 API 調用或兼容代碼挖的坑,不一定是 Pointer 的問題

關於 GetPointerDevices 的描述,請參閱 https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getpointerdevices

簡單的 GetPointerDevices 用法可以使用 PInvoke 調用,如下面例子

先安裝 Microsoft.Windows.CsWin32 庫,如 dotnet 使用 CsWin32 庫簡化 Win32 函數調用邏輯 博客提供的方法

接下來編寫代碼從 GetPointerDevices 裏獲取觸摸信息

                StringBuilder stringBuilder = ...

                // 獲取 Pointer 設備數量
                uint deviceCount = 0;
                PInvoke.GetPointerDevices(ref deviceCount,
                    (Windows.Win32.UI.Controls.POINTER_DEVICE_INFO*)IntPtr.Zero);
                Windows.Win32.UI.Controls.POINTER_DEVICE_INFO[] pointerDeviceInfo =
                    new Windows.Win32.UI.Controls.POINTER_DEVICE_INFO[deviceCount];
                fixed (Windows.Win32.UI.Controls.POINTER_DEVICE_INFO* pDeviceInfo = &pointerDeviceInfo[0])
                {
                    // 這裏需要拿兩次,第一次獲取數量,第二次獲取信息
                    PInvoke.GetPointerDevices(ref deviceCount, pDeviceInfo);
                    stringBuilder.AppendLine($"PointerDeviceCount:{deviceCount} 設備列表:");
                    foreach (var info in pointerDeviceInfo)
                    {
                        stringBuilder.AppendLine($" - {info.productString}");
                    }
                }

需要調用 GetPointerDevices 兩次,第一個獲取數量,第二次獲取信息。這個 GetPointerDevices 在第一個參數傳入是 0 的時候,是不會填充第二個參數數組信息

以上就是專爲 WPF 觸摸模塊提供的 ITabletManager 的 GetTabletCount 方法在 Windows 11 系統的底層實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章