dotnet 读 WPF 源代码笔记 插入触摸设备的初始化获取设备信息
在 WPF 觸摸應用中,插入觸摸設備,即可在應用里面使用上插入的觸摸設備。在 WPF 使用觸摸設備的觸摸時,需要獲取到觸摸設備的信息,才能實現觸摸
獲取觸摸設備插入
在 WPF 中,通過 Windows 消息獲取觸摸設備插入事件,在 src\Microsoft.DotNet.Wpf\src\PresentationCore\System\Windows\Input\Stylus\Wisp\WispLogic.cs 的 HandleMessage 將獲取 Windows 消息,代碼如下
internal override void HandleMessage(WindowMessage msg, IntPtr wParam, IntPtr lParam){switch (msg){// 忽略代碼case WindowMessage.WM_TABLET_ADDED:OnTabletAdded((uint)NativeMethods.IntPtrToInt32(wParam));break;case WindowMessage.WM_TABLET_DELETED:OnTabletRemovedImpl((uint)NativeMethods.IntPtrToInt32(wParam), isInternalCall: true);break;}}在 WPF 框架,使用 WM_TABLET_ADDED 和 WM_TABLET_DELETED 消息獲取設備的插入和刪除事件
如上面代碼,在設備插入時,將會調用 OnTabletAdded 方法。如 WM_TABLET_ADDED 官方文檔描述,以上代碼獲取的參數是 Wisptis 的 Index 序號。這是因為用戶可以插入多個觸摸設備,通過傳入序號可以拿到插入的設備
在 WPF 中,每次插入觸摸設備,都會重新更新所有的觸摸設備的信息,而不是只更新插入的設備。在 OnTabletAdded 方法里面,將會調用 GetDeviceCount 方法,在 GetDeviceCount 方法里面將通過 PenThread 的 WorkerGetTabletsInfo 更新所有觸摸設備的信息,代碼如下
private void OnTabletAdded(uint wisptisIndex){lock (__penContextsLock){WispTabletDeviceCollection tabletDeviceCollection = WispTabletDevices;// 忽略代碼// Update the last known device count._lastKnownDeviceCount = GetDeviceCount();uint tabletIndex = UInt32.MaxValue;// HandleTabletAdded returns true if we need to update contexts due to a change in tablet devices.if (tabletDeviceCollection.HandleTabletAdded(wisptisIndex, ref tabletIndex)){// Update all contexts with this new tablet device.foreach (PenContexts contexts in __penContextsMap.Values){contexts.AddContext(tabletIndex);}}}}private int GetDeviceCount(){PenThread penThread = null;// Get a PenThread by mimicking a subset of the code in TabletDeviceCollection.UpdateTablets().TabletDeviceCollection tabletDeviceCollection = TabletDevices;if (tabletDeviceCollection != null && tabletDeviceCollection.Count > 0){penThread = tabletDeviceCollection[0].As<WispTabletDevice>().PenThread;}if (penThread != null){// Use the PenThread to get the full, unfiltered tablets info to see how many there are.TabletDeviceInfo[] tabletdevices = penThread.WorkerGetTabletsInfo();return tabletdevices.Length;}else{// if there's no PenThread yet, return "unknown"return -1;} } // WPF 代碼格式化就是這樣以上代碼調用 WorkerGetTabletsInfo 方法實際的獲取觸摸信息邏輯是放在觸摸線程,上面代碼需要先獲取觸摸線程 PenThread 然后調用觸摸線程類的 WorkerGetTabletsInfo 方法,在這個方法里面執行邏輯
觸摸線程
在 WPF 觸摸到事件 博客里面告訴大家,在 WPF 框架,為了讓觸摸的性能足夠強,將觸摸的獲取放在獨立的進程里面
在獲取觸摸信息時,也需要調度到觸摸線程執行。在 WPF 中,通過 PenThread 類的相關方法可以調度到觸摸線程
在調用 WorkerGetTabletsInfo 方法時,進入 WorkerGetTabletsInfo 方法依然是主線程,里面代碼如下
internal TabletDeviceInfo[] WorkerGetTabletsInfo(){// Set data up for this callWorkerOperationGetTabletsInfo getTablets = new WorkerOperationGetTabletsInfo();lock(_workerOperationLock){_workerOperation.Add(getTablets);}// Kick thread to do this work.MS.Win32.Penimc.UnsafeNativeMethods.RaiseResetEvent(_pimcResetHandle.Value);// Wait for this work to be completed.getTablets.DoneEvent.WaitOne();getTablets.DoneEvent.Close();return getTablets.TabletDevicesInfo;}實際上以上代碼是放在 PenThreadWorker.cs 文件中,在 WPF 的觸摸線程設計上,觸摸線程是一個循環,將會等待 PenImc 層發送觸摸消息,或者等待 _pimcResetHandle 鎖被釋放。如上面代碼,先插入 WorkerOperationGetTabletsInfo 到 _workerOperation 列表中,然后調用 RaiseResetEvent 方法釋放 _pimcResetHandle 對象。觸摸線程將會因為 _pimcResetHandle 被釋放而跳出循環,然后獲取 _workerOperation 列表里面的項,進行執行邏輯
主線程將會在 getTablets.DoneEvent.WaitOne 方法里面進入鎖,等待觸摸線程執行 WorkerOperationGetTabletsInfo 完成之后釋放這個鎖,才能讓主線程繼續執行
觸摸線程的循環邏輯代碼大概如下
internal void ThreadProc(){Thread.CurrentThread.Name = "Stylus Input";while (!__disposed){// 忽略代碼WorkerOperation[] workerOps = null;lock(_workerOperationLock){if (_workerOperation.Count > 0){workerOps = _workerOperation.ToArray();_workerOperation.Clear();}}if (workerOps != null){for (int j=0; j<workerOps.Length; j++){workerOps[j].DoWork();}workerOps = null;}// 這是第二層循環while (true){// 忽略代碼if (!MS.Win32.Penimc.UnsafeNativeMethods.GetPenEvent(_handles[0], _pimcResetHandle.Value,out evt, out stylusPointerId,out cPackets, out cbPacket, out pPackets)){break;}}}默認 WPF 的觸摸線程都會在第二層循環,在 GetPenEvent 方法里面等待 PenImc 發送觸摸消息或等待 _pimcResetHandle 釋放。在跳出第二層循環,將會去獲取 _workerOperation 的項,然后執行
WorkerOperation[] workerOps = null;lock(_workerOperationLock){if (_workerOperation.Count > 0){workerOps = _workerOperation.ToArray();_workerOperation.Clear();}}if (workerOps != null){for (int j=0; j<workerOps.Length; j++){workerOps[j].DoWork();}workerOps = null;}獲取觸摸信息
在調用 WorkerOperationGetTabletsInfo 的 DoWork 方法時,將會在觸摸線程獲取觸摸設備信息
private class WorkerOperationGetTabletsInfo : WorkerOperation{internal TabletDeviceInfo[] TabletDevicesInfo{get { return _tabletDevicesInfo;}}//// <summary>/// Returns the list of TabletDeviceInfo structs that contain information/// about all of the TabletDevices on the system./// </summary>protected override void OnDoWork(){try{// create new collection of tabletsMS.Win32.Penimc.IPimcManager3 pimcManager = MS.Win32.Penimc.UnsafeNativeMethods.PimcManager;uint cTablets;pimcManager.GetTabletCount(out cTablets);TabletDeviceInfo[] tablets = new TabletDeviceInfo[cTablets];for ( uint iTablet = 0; iTablet < cTablets; iTablet++ ){MS.Win32.Penimc.IPimcTablet3 pimcTablet;pimcManager.GetTablet(iTablet, out pimcTablet);tablets[iTablet] = PenThreadWorker.GetTabletInfoHelper(pimcTablet);}// Set result data and signal we are done._tabletDevicesInfo = tablets;}catch (Exception e) when (PenThreadWorker.IsKnownException(e)){Debug.WriteLine("WorkerOperationGetTabletsInfo.OnDoWork failed due to: {0}{1}", Environment.NewLine, e.ToString());}}TabletDeviceInfo[] _tabletDevicesInfo = Array.Empty<TabletDeviceInfo>();}上面代碼的 IPimcManager3 接口是一個 COM 接口,實際邏輯是在 PenImc 層進行定義,在 PenImcRcw.cs 引用,代碼如下
[ComImport,Guid(PimcConstants.IPimcManager3IID),InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]interface IPimcManager3{void GetTabletCount(out UInt32 count);void GetTablet(UInt32 tablet, out IPimcTablet3 IPimcTablet);}在 PenImc 層的 PenImc.idl 文件里面,定義了公開的接口
[object,uuid(BD2C38C2-E064-41D0-A999-940F526219C2),nonextensible,helpstring("IPimcManager3 Interface"),pointer_default(unique) ] interface IPimcManager3 : IUnknown {[helpstring("method GetTabletCount")] HRESULT GetTabletCount([out] ULONG* pcTablets);[helpstring("method GetTablet") ] HRESULT GetTablet([in] ULONG iTablet, [out] IPimcTablet3** ppTablet); };在 WPF 中,在 C# 代碼使用的不是最底層的方法,也就是 BD2C38C2-E064-41D0-A999-940F526219C2 組件只是 WPF 用的,而不是系統等給的接口
實際調用底層的代碼是在 PenImc 層的 C++ 代碼,但 PenImc 層的 C++ 代碼只是一層轉發調用而已,換句話說,如果使用 C# 調用底層的系統的組件也是完全可以的
如上面代碼通過 GetTabletCount 方法獲取當前的觸摸設備,此方法是通過 COM 調用到在 PenImc.idl 文件定義的 GetTabletCount 獲取的,實際定義的代碼是 PimcManager.cpp 文件的 GetTabletCount 方法
STDMETHODIMP CPimcManager::GetTabletCount(__out ULONG* pcTablets) {DHR;ULONG cTablets = 0;LoadWisptis(); // Try to load wisptis via the surrogate object.// we will return 0 in the case that there is no stylus since mouse is not considered a stylus anymoreif (m_fLoadedWisptis){CHR(m_pMgrS->GetTabletCount(&cTablets));}*pcTablets = cTablets;CLEANUP:RHR; }以上代碼里面用到了一些宏,如 DHR 的含義是定義 HRESULT 變量,代碼如下
#define DHR \HRESULT hr = S_OK;而 CHR 表示的是判斷 HRESULT 的值,如果失敗了,將會調用 CLEANUP 標簽的內容。在 CHR 里面用到 goto 的方法
#define CHR(hr_op) \{ \hr = hr_op; \if (FAILED(hr)) \goto CLEANUP; \}上面代碼的 RHR 表示的是返回 HRESULT 變量
#define RHR \return hr;因此以上代碼實際就是如下代碼
STDMETHODIMP CPimcManager::GetTabletCount(__out ULONG* pcTablets) {HRESULT hr = S_OK;ULONG cTablets = 0;LoadWisptis(); // Try to load wisptis via the surrogate object.// we will return 0 in the case that there is no stylus since mouse is not considered a stylus anymoreif (m_fLoadedWisptis){hr = m_pMgrS->GetTabletCount(&cTablets);if (FAILED(hr)){goto CLEANUP;}}*pcTablets = cTablets;CLEANUP:return hr; }通過上面代碼可以看到,實際調用的是 m_pMgrS 的 GetTabletCount 方法,也就是如下代碼定義的方法
MIDL_INTERFACE("764DE8AA-1867-47C1-8F6A-122445ABD89A")ITabletManager : public IUnknown{public:virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetDefaultTablet( /* [out] */ __RPC__deref_out_opt ITablet **ppTablet) = 0;virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetTabletCount( /* [out] */ __RPC__out ULONG *pcTablets) = 0;virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetTablet( /* [in] */ ULONG iTablet,/* [out] */ __RPC__deref_out_opt ITablet **ppTablet) = 0;virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetTabletContextById( /* [in] */ TABLET_CONTEXT_ID tcid,/* [out] */ __RPC__deref_out_opt ITabletContext **ppContext) = 0;virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE GetCursorById( /* [in] */ CURSOR_ID cid,/* [out] */ __RPC__deref_out_opt ITabletCursor **ppCursor) = 0;};可以看到這是一個 COM 接口調用,實際使用的就是系統提供的 ITabletManager 組件
在底層系統組件,先調用 ITabletManager 的 GetTabletCount 方法 獲取觸摸設備數量,然后遍歷觸摸設備序號拿到 ITablet 對象
在 C# 代碼里面的邏輯如下
pimcManager.GetTabletCount(out cTablets);TabletDeviceInfo[] tablets = new TabletDeviceInfo[cTablets];for ( uint iTablet = 0; iTablet < cTablets; iTablet++ ){MS.Win32.Penimc.IPimcTablet3 pimcTablet;pimcManager.GetTablet(iTablet, out pimcTablet);tablets[iTablet] = PenThreadWorker.GetTabletInfoHelper(pimcTablet);}這里的 pimcManager.GetTablet 方法將會調用到 PimcManager.cpp 的 GetTablet 方法
STDMETHODIMP CPimcManager::GetTablet(ULONG iTablet, __deref_out IPimcTablet3** ppTablet) {DHR;switch (iTablet){case RELEASE_MANAGER_EXT:{CHR(m_managerLock.Unlock());}break;default:{CHR(GetTabletImpl(iTablet, ppTablet));}}CLEANUP:RHR; }STDMETHODIMP CPimcManager::GetTabletImpl(ULONG iTablet, __deref_out IPimcTablet3** ppTablet) {DHR;LoadWisptis(); // Make sure wisptis has been loaded! (Can happen when handling OnTabletAdded message)CComPtr<ITablet> pTabS;CComObject<CPimcTablet> * pTabC;// Can only call if we have real tablet hardware which means wisptis must be loaded!CHR(m_fLoadedWisptis ? S_OK : E_UNEXPECTED);CHR(CComObject<CPimcTablet>::CreateInstance(&pTabC));CHR(pTabC->QueryInterface(IID_IPimcTablet3, (void**)ppTablet));CHR(m_pMgrS->GetTablet(iTablet, &pTabS));CHR(pTabC->Init(m_fLoadedWisptis?pTabS:NULL, this));CLEANUP:RHR; }本質調用的是 m_pMgrS 的 GetTablet 方法,也就是系統提供的 ITabletManager 的 GetTablet 方法 獲取 ITablet 接口。只是在 C++ 代碼里面,將 ITablet 接口再做一層封裝,返回給 C# 的是 IPimcTablet3 接口
接下來就是通過 PenThreadWorker 的 GetTabletInfoHelper 方法獲取觸摸信息
private static TabletDeviceInfo GetTabletInfoHelper(IPimcTablet3 pimcTablet){TabletDeviceInfo tabletInfo = new TabletDeviceInfo();tabletInfo.PimcTablet = new SecurityCriticalDataClass<IPimcTablet3>(pimcTablet);pimcTablet.GetKey(out tabletInfo.Id);pimcTablet.GetName(out tabletInfo.Name);pimcTablet.GetPlugAndPlayId(out tabletInfo.PlugAndPlayId);int iTabletWidth, iTabletHeight, iDisplayWidth, iDisplayHeight;pimcTablet.GetTabletAndDisplaySize(out iTabletWidth, out iTabletHeight, out iDisplayWidth, out iDisplayHeight);tabletInfo.SizeInfo = new TabletDeviceSizeInfo(new Size(iTabletWidth, iTabletHeight),new Size(iDisplayWidth, iDisplayHeight));int caps;pimcTablet.GetHardwareCaps(out caps);tabletInfo.HardwareCapabilities = (TabletHardwareCapabilities)caps;int deviceType;pimcTablet.GetDeviceType(out deviceType);tabletInfo.DeviceType = (TabletDeviceType)(deviceType -1);// // REENTRANCY NOTE: Let a PenThread do this work to avoid reentrancy!// The IPimcTablet3 object is created in the pen thread. If we access it from the UI thread,// COM will set up message pumping which will cause reentrancy here.InitializeSupportedStylusPointProperties(pimcTablet, tabletInfo);tabletInfo.StylusDevicesInfo = GetStylusDevicesInfo(pimcTablet);// Obtain the WispTabletKey for future use in locking the WISP tablet.tabletInfo.WispTabletKey = MS.Win32.Penimc.UnsafeNativeMethods.QueryWispTabletKey(pimcTablet);// If the manager has not already been created and locked, we will lock it here. This is the first opportunity// we will have to lock the manager as it will have been created on the thread to instantiate the first tablet.MS.Win32.Penimc.UnsafeNativeMethods.SetWispManagerKey(pimcTablet);MS.Win32.Penimc.UnsafeNativeMethods.LockWispManager();return tabletInfo;}實際調用的就是 ITablet 接口 的方法
以上代碼的 pimcTablet.GetKey 方法是在 C++ 層封裝的,而不是系統提供的
STDMETHODIMP CPimcTablet::GetKey(__out INT * pKey) {DHR;CHR(pKey ? S_OK : E_INVALIDARG);*pKey = (INT)PtrToInt(m_pTabS.p); CLEANUP:RHR; }CComPtr<ITablet> m_pTabS;在 WPF 框架,獲取的方法本質就是通過 Tablet PC 系統組件獲取
更多觸摸請看 WPF 觸摸相關
我搭建了自己的博客 https://blog.lindexi.com/ 歡迎大家訪問,里面有很多新的博客。只有在我看到博客寫成熟之后才會放在csdn或博客園,但是一旦發布了就不再更新
如果在博客看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎大家加入
如有不方便在博客評論的問題,可以加我 QQ 2844808902 交流
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名林德熙(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布。如有任何疑問,請與我聯系。
總結
以上是生活随笔為你收集整理的dotnet 读 WPF 源代码笔记 插入触摸设备的初始化获取设备信息的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: arcgis中的wgs84转西安80
- 下一篇: 工厂设计模式和抽象工厂设计模式