在 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 call
WorkerOperationGetTabletsInfo 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 tablets
MS.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 anymore
if (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 anymore
if (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://www.uj5u.com/net/285419.html
標籤:WPF
