“阻断疗法” - 拯救 WPF 启动过程中发生设备热插拔导致触摸失效问题


如果你在WPF程序启动过程中进行设备热插拔(例如,插入一个U盘,一个USB摄像头),那么你的WPF程序很有可能失去所有触摸消息响应,通过 Tablet.TabletDevices.Count 检查当前程序的挂靠触摸设备,发现为0。有趣的是,如果你将触摸线重新插拔后,程序恢复正常。所以,这是WPF的Bug,微软的锅。那么这个锅的根本原因是啥?有兴趣的可以调试 .net framework 源码,这里没有深究。
如上面讲到,触摸线重新插拔就可以解决这个问题,但是,导致这个问题的热插拔设备也不是触摸设备啊,只是一个普通的U盘,反过来想,如果导致问题的不是触摸设备热插拔,反而触摸设备的热插拔能够修复这个问题,那我能不能“模拟”一下触摸设备的热插拔事件呢?在这篇文章里描述怎样模拟触摸设备移除事件来达到禁用WPF触摸的效果,反过来试试,通过 OnTabletAdded 事件看看能不能发生奇迹。然而,奇迹并没有发生,所以这个方法不行。
既然模拟设备添加事件的方法不行,那我从源头阻挡这个问题的发生:启动过程中不要处理设备变动事件。那么问题来了,我想要阻断 win32 WM 事件通知,必须要拿到一个窗口句柄呀,但是在 mainwindow show 出来的时候,这个问题已经发生了,这个时候的阻断已经没有效果了,一定要程序启动一开始做阻断。进一步搜索,这里https://stackoverflow.com/questions/38642479/how-to-disable-wpf-tablet-support-in-surface-4-pro 是一个突破口:

WPF does not register to these messages on the applications MainWindow, but through a hidden windows named “SystemResources…” which is created for each application instance. So handling those messages on the MainWindow (which would be easy) does not help here.

相信看到这里,聪明的你已经知道怎么做了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class WPFTouchUtil
{
//添加钩子,阻断设备改动消息
public static void HandleDeviceChangedWM()
{
// hook into internal class SystemResources to keep it from updating the TabletDevices on system events
object hwndWrapper = GetSystemResourcesHwnd();
if (hwndWrapper != null)
{
// invoke hwndWrapper.AddHook( .. our method ..)
var internalHwndWrapperType = hwndWrapper.GetType();
// if the delegate is already set, we have already added the hook.
if (_handleAndHideMessageDelegate == null)
{
// create the internal delegate that will hook into the window messages
// need to hold a reference to that one, because internally the delegate is stored through a WeakReference object
var internalHwndWrapperHookDelegate = internalHwndWrapperType.Assembly.GetType("MS.Win32.HwndWrapperHook");
var handleAndHideMessagesHandle = typeof(WPFTouchUtil).GetMethod(nameof(HandleAndHideMessages), BindingFlags.Static | BindingFlags.NonPublic);
_handleAndHideMessageDelegate = Delegate.CreateDelegate(internalHwndWrapperHookDelegate, handleAndHideMessagesHandle);
// add a delegate that handles WM_TABLET_ADD
internalHwndWrapperType.InvokeMember("AddHook",
BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public,
null, hwndWrapper, new object[] { _handleAndHideMessageDelegate });
}
}
}
//移除钩子,恢复状态
public static void RestoreDeviceChangedWM()
{
object hwndWrapper = GetSystemResourcesHwnd();
if (hwndWrapper != null)
{
var internalHwndWrapperType = hwndWrapper.GetType();
internalHwndWrapperType.InvokeMember("RemoveHook",
BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public,
null, hwndWrapper, new object[] {_handleAndHideMessageDelegate});
}
}
private static Delegate _handleAndHideMessageDelegate = null;
private static object GetSystemResourcesHwnd()
{
var internalSystemResourcesType = typeof(Application).Assembly.GetType("System.Windows.SystemResources");
// get HwndWrapper from internal property SystemRessources.Hwnd;
var hwndWrapper = internalSystemResourcesType.InvokeMember("Hwnd",
BindingFlags.GetProperty | BindingFlags.Static | BindingFlags.NonPublic,
null, null, null);
return hwndWrapper;
}
private static IntPtr HandleAndHideMessages(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == (int)WindowMessage.WM_DEVICECHANGE)
{
handled = true;
}
return IntPtr.Zero;
}
enum WindowMessage : int
{
WM_DEVICECHANGE = 0x0219
}
}

在程序刚启动的时候添加“阻断”,启动流程过后,不要忘了恢复状态。
缺陷,如果程序启动过程中,真的发生了触摸设备变动,也会被阻断。

评论