WPF 插拔触摸设备触摸失效

原文:WPF 插拔触摸设备触摸失效

最近使用 WPF 程序,在不停插拔触摸设备会让 WPF 程序触摸失效。通过分析 WPF 源代码可以找到 WPF 触摸失效的原因。

在 Windows 会将所有的 Hid (输入)设备在插拔的时候通过 Windows 消息进行更新设备信息。在触摸的时候,通过一个线程用来收集触摸信息。

本文下面的代码是通过 dnSpy反编译调试。

存在的问题

通过 WPF 的源代码可以发现存在两个问题,两个问题在两个不同的类

  • 在 PenThreadWorker 的 GetPenEventMultiple 传入 _handles 为空数组,并且进行无限的等待
  • WorkerOperationGetTabletsInfo 的 OnDoWork 出现 COMException 返回空的数组,原本存在触摸设备识别为不存在

解决方法

在重新插拔触摸屏就可以恢复。

通过降低 CPU 频率可以减少触摸失效

原理

需要从 WPF 代码解释为什么会出现这个问题

在 WPF 的 PresentationCore.dll 有一个类 PenThreadWorker 就是用来收集用户的触摸。这个类的构造函数创建了一个线程,请看下面代码

			new Thread(new ThreadStart(this.ThreadProc))
			{
				IsBackground = true
			}.Start();

这个线程的实际代码是 ThreadProc 函数,在这个函数的第一句话是设置这个线程的线程名为Stylus Input 然后进入一个循环

while (!this.__disposed)
{
	// 初始化触摸设备代码

    for (;;)
    {
		// 这是获取触摸流程循环
		// 获取用户触摸代码
    }
}

这个 ThreadProc 有两层循环,初始化流程循环用于在初始化触摸设备,用于更新 PenContext 的值。获取触摸流程循环用于拿到用户触摸相关。很多的时候,在用户正常使用的流程只是运行初始化流程循环一次,之后在用户触摸的时候就通过获取触摸流程循环拿到值。本文下面就会使用初始化流程循环指代第一层循环,使用获取触摸流程循环指代第二层循环。

获取触摸流程循环是通过 penimc2_v0400.dll 来拿到触摸收集进程收集到的点。

在程序开始运行到UnsafeNativeMethods.GetPenEvent(this._handles[0], this._pimcResetHandle.Value, out num, out stylusPointerId, out cPackets, out cbPacket, out pPackets)线程就会在这里等待,一旦屏幕任何部分收到触摸信息,就会继续。线程等待的方法是因为在构造函数使用了 CreateResetEvent 创建信号量,创建信号量传入 GetPenEvent 在收到用户触摸时才释放,于是线程才可以继续运行。

上面的代码是只处理存在 一个 PenContext 的情况,如果存在多个 PenContext 就会运行 UnsafeNativeMethods.GetPenEventMultiple(this._handles.Length, this._h
t num, out stylusPointerId, out cPackets, out cbPacket, out pPackets)
的方法,这个方法和 GetPenEvent 一样在没有收到触摸信息时会卡住,一旦屏幕任何部分收到触摸信息,就会继续。这里的代码需要简单的介绍

for (;;)
{
	// 无限循环,直到发现设备更新
	int num;// 表示事件,事件是数值
	// 707:PenInRange
	// 708:PenOutOfRange
	// 709:PenDown
	// 710:PenUp
	// 711:Packets
	int stylusPointerId;
	int cPackets;
	int cbPacket;
	IntPtr pPackets;
	int num2; // 是那个 PenContext 拿到触摸
	// 如果只有一个 PenContext 就使用下面代码
	if (this._handles.Length == 1)
	{
		if (!UnsafeNativeMethods.GetPenEvent(this._handles[0], this._pimcResetHandle.Value, out num, out stylusPointerId, out cPackets, out cbPacket, out pPackets))
		{
			// 如果更新触摸屏,这里会返回,到初始化流程循环

			break;
		}
		// 因为只有一个 PenContext 于是使用数组第一个
		num2 = 0;
	}
	// 如果有比较多个 PenContext 就使用下面代码
	else if (!UnsafeNativeMethods.GetPenEventMultiple(this._handles.Length, this._handles, this._pimcResetHandle.Value, out num2, out num, out stylusPointerId, out cPackets, out cbPacket, out pPackets))
	{
		// 如果更新触摸屏,这里会返回,到初始化流程循环

		break;
	}
	if (num != 1)
	{
		// 拿到用户触摸的所在 penContext 这个 _penContexts 是弱引用
		PenContext penContext = this._penContexts[num2].Target as PenContext;
		if (penContext != null)
		{
			// 执行 penContext 将触摸发送到框架
			this.FireEvent(penContext, num, stylusPointerId, cPackets, cbPacket, pPackets);
		}
	}
	else
	{
		this.FlushCache(true);
		for (int j = 0; j < this._penContexts.Length; j++)
		{
			PenContext penContext2 = this._penContexts[j].Target as PenContext;
			if (penContext2 != null)
			{
				penContext2.FirePenOutOfRange(0, Environment.TickCount);
			}
		}
	}
}

这个代码看起来可以很好工作,但是不要忘了,如果有用户在不停出现插拔触摸屏,会出现什么?

因为 GetPenEventGetPenEventMultiple 都只有在触摸屏收到触摸信息或者 _pimcResetHandle 被释放会返回,而在用户拔出触摸屏时,触摸屏是没有收到触摸信息,于是 GetPenEventGetPenEventMultiple 不会因为收到触摸信息返回。在 GetPenEventGetPenEventMultiple 收到触摸信息返回的是 true 而在释放 _pimcResetHandle 返回的是 false 于是上面代码就根据这个方法判断返回,如果是因为释放 _pimcResetHandle 就是因为更新了 _workerOperation 从初始化流程循环可以运行 _workerOperation 而不是等待触摸,这个过程在用户插拔触摸屏很有用。

从上面的代码可以知道,更新触摸的代码只会执行一次,因为在初始化流程循环会使用一个数组复制_workerOperation并且清空,然后运行。这个过程使用了锁,于是更新触摸的代码只会执行一次。

PenThreadWorker.WorkerOperation[] array = null;
// 这是一个锁,防止在这个线程运行时,修改 _workerOperation 的值
object workerOperationLock = this._workerOperationLock;
// 复制 _workerOperation 到数组,然后清空这个 _workerOperation 这样做是防止在其他线程等太久
// 如果不复制,直接执行  _workerOperation 而且执行的时间比较长,那么在其他线程需要修改 _workerOperation 就需要等很久,通过这个方法可以先复制到新的数组,复制完成就可以修改
lock (workerOperationLock)
{
	if (this._workerOperation.Count > 0)
	{
		array = this._workerOperation.ToArray();
		this._workerOperation.Clear();
	}
}
if (array != null)
{
	for (int i = 0; i < array.Length; i++)
	{
		array[i].DoWork();
	}
	array = null;
}

那么第二个问题,这个 _workerOperation 是从哪里赋值的?实际上调用 WorkerAddPenContext 函数或调用 WorkerRemovePenContext 函数时,就会修改这个值。这个函数就是在触摸屏插拔的时候触发。

下面是尝试在各个 _workerOperation 加入和运行的时候输出的日志

// 拔
14:51:24.458 WorkerGetTabletsInfo
14:51:24.462 Clear
14:51:24.468 WorkerRemovePenContext
14:51:24.472 Clear
14:51:24.475 WorkerGetTabletsInfo
14:51:24.480 Clear
14:51:24.493 WorkerReleaseTabletLocks
14:51:24.499 Clear
// 插
14:51:25.623 WorkerGetTabletInfo
14:51:25.628 Clear
14:51:25.632 WorkerAcquireTabletLocks
14:51:25.640 Clear
14:51:25.648 WorkerCreateContext
14:51:25.657 Clear
14:51:25.671 WorkerAddPenContext
14:51:25.677 Clear

在所有的设备修改都会触发 windows 消息,在 HwndWrapper.WndProc 可以收到 Windows 消息

如果收到的消息是 WM_TABLET_ADDEDWM_TABLET_DELETED 就会在 SystemResources.SystemThemeFilterMessage 调用 InvalidateTabletDevices 刷新触摸屏

    private static IntPtr SystemThemeFilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
    	WindowMessage message = (WindowMessage)msg;
        switch (message)
        {
        	// 其他忽略消息
            case WindowMessage.WM_TABLET_ADDED:
                 InvalidateTabletDevices(message, wParam, lParam);
                 break;

            case WindowMessage.WM_TABLET_DELETED:
                 InvalidateTabletDevices(message, wParam, lParam);
                 break;
        }
    }

InvalidateTabletDevices 函数是将消息转发 StylusLogic.CurrentStylusLogic?.HandleMessage(msg, wParam, lParam); 使用StylusLogic.HandleMessage 处理

这里因为 StylusLogic 调用基类 WispLogic 的 HandleMessage 方法,所以下面的代码是在WispLogic 运行。因为存在添加和移除设备两个不同的消息,所以下面分为两条路径。但是无论是哪条路径最后都是到 RefreshTablets 先禁用当前的 PenContext 再收集当前的 PenContext 然后启用

禁用 PenContext 的方法是调用 PenContext.Disable ,这个方法传入一个 bool 告诉是否关闭 WorkerThread 这里都是使用 false ,这里核心就是 _penThreadPenContext.RemovePenContext(this) 这里的 RemovePenContext 会调用 PenThreadWorker.WorkerRemovePenContext 这个函数不是立刻运行移除,这个函数先添加 _workerOperation 然后释放 _pimcResetHandle 等待 Stylus Input 处理完成再继续。

是否还记得刚才的 ThreadProc 获取触摸流程循环,在用户没有触摸时,假设只有一个 PenContext 会在 GetPenEvent 等待,等待的方法是通过 _pimcResetHandle ,现在在 WorkerRemovePenContext 函数释放了,于是 GetPenEvent 就会返回。这里返回是 false 于是退出获取触摸流程循环,进入初始化流程循环,这时的 _workerOperation 有一个值 WorkerOperationRemoveContext。在初始化流程循环使用 WorkerOperationRemoveContext.OnDoWork 通过 DoWork 使用 RemovePenContext 移除。这里就是 RemovePenContext 移除 PenContext 的过程。

因为上面说了很多细节,但是从代码看,这个流程从主线程通过先添加 _workerOperation 加入如何处理的代码,然后通过释放 _pimcResetHandle 让处理输入的线程退出获取触摸流程循环,在初始化流程循环处理。

收集当前的 PenContext 是使用 WispTabletDevices.UpdateTablets 请看代码


WispLogic.cs

		private void RefreshTablets()
		{
			foreach (PenContexts penContexts in this.__penContextsMap.Values)
			{
				penContexts.Disable(false);
			}
			this.WispTabletDevices.UpdateTablets();
			foreach (PenContexts penContexts2 in this.__penContextsMap.Values)
			{
				penContexts2.Enable();
			}
		}

在 UpdateTablets 实际是调用 UpdateTabletsImpl 这里有一句代码是 penThread.WorkerGetTabletsInfo ,而 penThread.WorkerGetTabletsInfo 是调用penThreadWorker.WorkerGetTabletsInfo 这里会释放 _pimcResetHandle ,也就是在 ThreadProc 的获取触摸流程循环就会退出。

		[SecurityCritical]
		internal TabletDeviceInfo[] WorkerGetTabletsInfo()
		{
			PenThreadWorker.WorkerOperationGetTabletsInfo workerOperationGetTabletsInfo = new PenThreadWorker.WorkerOperationGetTabletsInfo();
			object workerOperationLock = this._workerOperationLock;
			lock (workerOperationLock)
			{
				this._workerOperation.Add(workerOperationGetTabletsInfo);
			}
			// 这里释放 _pimcResetHandle 会让 ThreadProc 的 GetPenEvent 等代码返回
			UnsafeNativeMethods.RaiseResetEvent(this._pimcResetHandle.Value);
			workerOperationGetTabletsInfo.DoneEvent.WaitOne();
			workerOperationGetTabletsInfo.DoneEvent.Close();
			return workerOperationGetTabletsInfo.TabletDevicesInfo;
		}

这里 WorkerOperationGetTabletsInfo 就是更新的代码,这里使用 COM 通过CoCreateInstance 新建 PimcManager 通过 PenThreadWorker.GetTabletInfoHelper 拿到 _tabletDevicesInfo 请看代码

            protected override void OnDoWork()
            {
                try
                {
                    // create new collection of tablets
                    MS.Win32.Penimc.IPimcManager pimcManager = MS.Win32.Penimc.UnsafeNativeMethods.PimcManager;
                    pimcManager.GetTabletCount(out var cTablets);

                    TabletDeviceInfo[] tablets = new TabletDeviceInfo[cTablets];

                    for ( uint iTablet = 0; iTablet < cTablets; iTablet++ )
                    {
                        MS.Win32.Penimc.IPimcTablet pimcTablet;
                        pimcManager.GetTablet(iTablet, out pimcTablet);

                        // 出现 System.Runtime.InteropServices.COMException
                        // 0x800706BA
                        tablets[iTablet] = PenThreadWorker.GetTabletInfoHelper(pimcTablet);
                    }

                    // Set result data and signal we are done.
                    _tabletDevicesInfo = tablets;
                }
                catch ( System.Runtime.InteropServices.COMException )
                {
                    // result will not be initialized if we fail due to a COM exception.
                    Debug.WriteLine("WorkerOperationGetTabletsInfo.OnDoWork failed due to a COMException");

                    // return no devices found on error.
                    _tabletDevicesInfo = new TabletDeviceInfo[0];
                }
                // 其他异常
            }

如果在 GetTabletInfoHelper 出现 COMExceptionArgumentException就会返回空的数组,因为无法拿到触摸设备,在 WPF 触摸失效。

下面的图片就是在普通的插拔触摸屏和快速插拔触摸屏时不同的输出,可以看到在快速插拔的时候在 GetTabletInfoHelper 出现了 ArgumentException 这时就会让 WPF 无法触摸

因为在 WispLogic 的 RefreshTablets 先禁用了 PenContexts 但是在更新 UpdateTablets 返回的是空数组,于是重新打开 PenContexts 就找不到触摸屏,所以就触摸失效

代码存在的问题

在上面所说的 GetTabletInfoHelper 出现 COMException 返回数组会出现异常会让 WPF 触摸失效。因为在用户插入触摸屏时触发了 TabletAdded 消息,在之后用户触摸时不会有其他的插拔触摸相关消息。所以在第一次收到WM_TABLET_ADDED做初始化时,如果出现了 COMException 等返回空数组就会失去初始化时机。

第二个问题是因为消息循环和获得输入的线程是两个线程,两个线程之间比较难做到同步,特别是使用释放 _pimcResetHandle 的方法让输入线程重新调用 _workerOperation ,在插拔触摸屏需要触发多个 Windows 消息,几乎每个消息都需要插入一个 _workerOperation 并且使用等待的方式。只要出现线程调度没有处理好,就会出现原来需要添加的 PenContext 没有添加,这时就无法获得触摸。

除了调度的时机,在 Stylus Input 线程是使用 _pimcResetHandle.ValueGetPenEvent 等函数进行等待。而 _workerOperationWorkerGetTabletsInfo 是从主线程的消息循环进入的,这个函数是将 WorkerOperationGetTabletsInfo 加入到 _workerOperation 然后释放 _pimcResetHandle.Value 通过释放 _pimcResetHandle.ValueStylus Input 线程就可以从 GetPenEvent 等函数返回,这时进入循环执行 _workerOperation ,然后主线程在等待 WorkerOperationGetTabletsInfo 的完成才可以继续代码。这样也就是原来的主线程是需要通过释放 _pimcResetHandle.ValueStylus Input 线程运行一段代码,并且主线程需要等待 Stylus Input 线程运行完成才可以继续。而我看了里面的代码,实在没必要让这些代码在 Stylus Input 线程运行。现在 WPF 这些代码的性能会比直接在主线程运行要低,因为代码运行的时间是 主线程释放 _pimcResetHandle 等待 Stylus Input 线程运行完成,等待 Stylus Input 线程运行完成需要等待 CPU 调度 Stylus Input 线程运行代码,还需要等待 Stylus Input 线程运行完成之后设置AutoResetEvent这个等待的时间相对直接在主线程运行是长很多,而且只要 CPU 一直不调度 Stylus Input 线程就需要一直等,不如重新创建线程。

第三个问题是在 PenThreadWorkerThreadProc 的获取触摸流程循环只判断 _handles.Length == 1 时调用 GetPenEvent 其他的时候调用 GetPenEventMultiple 也就是在 _handles.Length 为 0 时也是调用 GetPenEventMultiple 这从开发的 API 设计上看,如果没有 _handles 也就是没有 PenContext 就不应该进入 GetPenEventMultiple 函数。建议是没有 _handles 使用另外的函数进行等待。

这里为什么在 _handles.Length 不是 1 需要使用 GetPenEventMultiple 而不是直接返回的原因是觉得创建线程的代价太高,或如果不进入等待的函数就会进入循环,不停进入循环。实际上这里在 _handles 没有值就是用户没有触摸屏,用户插入触摸屏的时间是很少的,没有几个用户一天没事都在插入拔出触摸屏,所以在用户插入触摸屏时再创建一个新的线程,在用户拔出触摸屏去掉这个线程是可以的。 通过这样做减少在这个线程等待的时间,防止用户的软件因为在 GetPenEventMultiple 没有释放触摸失效。

但是无论是什么方法都难以解决所有触摸问题,建议开发接口让应用去修改触摸相关的,如重新进行初始化触摸

参见:

WPF and Custom Credential Provider in Windows 7

.net - InvalidCastException with COM client and server in C# - Stack Overflow

WPF 程序无法触摸操作?我们一起来找原因和解决方法

原文地址:https://www.cnblogs.com/lonelyxmas/p/10218428.html

时间: 2024-08-03 00:42:02

WPF 插拔触摸设备触摸失效的相关文章

触屏设备触摸事件实验和记录

1. 事件概述 触屏设备的原始触摸事件有四个,[touchstart][touchmove][touchend][touchcancle],这里只讲前三个,因为最后一个是被动被打断时冒出的事件. 2. touchstart 用户把手指放到屏幕上的一刹那,触发的事件. 3. touchmove 用户用手指在屏幕上滑动,会持续触发这个事件. 4. touchend 用户的手指离开屏幕时触发的事件. 5. 属性概述 每一次触发事件,这个事件会 attach 一些 properties,就是附带一些额外

2019-11-29-WPF-模拟触摸设备

原文:2019-11-29-WPF-模拟触摸设备 title author date CreateTime categories WPF 模拟触摸设备 lindexi 2019-11-29 08:47:53 +0800 2019-5-11 17:2:6 +0800 WPF 在 WPF 中触摸只是框架的一层,可以通过代码模拟触摸 创建一个类继承 TouchDevice 然后重写 GetTouchPoint 和 GetIntermediateTouchPoints 方法,可以在这个类里面通过调用 R

支持移动触摸设备的纯js元素拖放插件

Dragula是一款支持移动触摸屏设备的元素拖放纯js插件.这个元素拖放插件使用简单,浏览器兼容性好,能够实现通过鼠标或在移动设备中通过手指来拖动DOM元素的位置.它的特点有: 设置非常简单 没有外部依赖 可以自动对数据进行排序 被移动项带有半透明的视觉效果 支持移动触摸设备 兼容性好,支持IE7+的所有现代浏览器 效果演示:http://www.htmleaf.com/Demo/201504171695.html 下载地址:http://www.htmleaf.com/jQuery/Layou

支持移动触摸设备的简洁js幻灯片插件

lory是一款支持移动触摸设备的简洁的js幻灯片插件.该幻灯片插件可以通过纯js调用,也可以将该幻灯片插件作为jquery插件来使用.该幻灯片的过渡动画具有硬件加速功能,并且可以定制是否使用easing效果. 以下列出该幻灯片的一些特点: 支持移动触摸设备. 简单,界面整洁,纯js调用. 可以作为jQuery插件来使用. 过渡效果支持硬件加速. 可定制easing效果. 可无限循环,制作为旋转木马. 丰富的回调函数. 效果演示:http://www.htmleaf.com/Demo/201504

如何监测耳机/麦克风设备插拔操作

没事的时候想起来做这么个功能,主要是想在插入耳机之后能自动调节到较小的音量,免得耳朵被震得难受. 在搜索过程中先搜到了CSDN论坛的一篇帖子<C#监测音频设备热插拔功能>(http://bbs.csdn.net/topics/390502788) 虽然没有靠这篇帖子解决问题,但是帖子还是指出了道路,就是用Windows Vista之后提供的新Core Audio API来侦测 在这里我是用NAudio(http://naudio.codeplex.com/)这个开源库中CoreAudio的实现

C#.NET U盘插拔监控

[1]涉及的知识点 1) windows消息处理函数 ? 1 protected override void WndProc(ref Message m) 捕获Message的系统硬件改变发出的系统消息 2) 硬件信息类 ? 1 DriveInfo [2]核心函数 消息常量: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /// <summary> /// windows消息常量 /// </summary> class CWn

MFC检测串口插拔

以对话框工程为例 头文件中 afx_msg BOOL OnDeviceChange(UINT nEventType, DWORD dwData); cpp中 ON_WM_DEVICECHANGE() 然后实现函数 BOOL CXXDlg::OnDeviceChange(UINT nEventType,DWORD dwData) { //0x4d36e978L, 0xe325, 0x11ce, 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 //DEV

Windows 下编程检测显示器信息及插拔

Windows下提示显示器信息主要通过两个函数实现.一个是EnumDisplayDevices(), 另一个是EnumDisplayMonitors(). EnumDisplayDevices()枚举所有显示设备,而EnumDisplayMonitors枚举的是所有显示器.显示设备和显示器不一样,比如显卡算显示设备,但是不是显示器.具体差别后面会分析.EnumDisplayMonitors()还会枚举出不可见的伪显示器,如果只是想得到实际的显示器数目的话可以用GetSystemMetrics(S

插入网线, network-manager 提示"线缆已拔出“, ”设备未托管“

修改 '/etc/NetworkManager/NetworkManager.conf', 将 'managed=false' 修改为 'managed=true' 修改 '/etc/network/interfaces', 在文件末尾添加有线网卡配置 auto eth0 iface eth0 inet dhcp 重启 network-manager 和 networking 服务 sudo service network-manager stop sudo service networking