使用 Windows 8 Interaction Context 处理 .NET WinForms 应用中的触摸输入






4.92/5 (12投票s)
介绍了 Interaction Context API 的托管包装器,并为 WinForms 的托管桌面应用程序提供了一种处理触摸输入的统一方式。
引言
Interaction Context 是 Windows 8 的一项新 API,专为构建 UI 框架的开发人员设计,这些框架可在桌面应用程序中提供一致的、针对触摸优化的用户体验。本文介绍了该 API 的托管包装器,以及在 WinForms 应用程序中使用 Interaction Context 的示例。
背景
如果您计划在桌面应用程序中支持触摸输入,有几种选择。
处理 WM_GESTURE
消息是最简单的选项。它与 Windows 7 兼容,并支持有用的手势,如平移、缩放和旋转。在平移时,可以将垂直移动限制在主要方向(使用沟槽),并使用惯性在平移手势停止时平滑减速。它支持双指轻触以及按压并轻触手势。但没有专用的轻触手势。您必须等待 WM_LBUTTONUP
消息,并使用 Win32 API 的 GetMessageExtraInfo()
函数区分鼠标和触摸输入。WM_GESTURE
消息的主要问题是它不支持多点触控交互。例如,您无法用一只手开始移动磁贴,同时用另一只手滚动磁贴列表,就像在 Windows 8 开始屏幕上一样。此外,您无法同时执行复杂的操纵,如平移、缩放和旋转。
WM_TOUCH
消息是一项更高级的功能。它也与 Windows 7 兼容,并提供了同时处理多个触摸输入的能力。然而,它不会检测任何高级手势。您只会获得一个 (x, y) 点数组和指示指定触摸点是已添加、已移动还是已移除的标志。这对某些应用程序来说可能足够了,例如钢琴模拟器之类的。对于其他应用程序来说,这还不够,您需要检测触摸手势。如果您使用 C++ 编程,可以使用标准的 IManipulationProcessor
和 IInertiaProcessor
COM 接口。它们功能强大,但需要实现事件接收器。我不知道是否有人尝试过从托管代码中使用这些接口。也许,创建托管对应项更有效,例如 .NET 4 中的 System.Windows.Input.Manipulations
命名空间中的类。无论如何,处理 WM_TOUCH
消息是棘手且容易出错的。它们还会伴随虚假的鼠标消息,您需要将它们与真实鼠标消息区分开来。
Windows 8 引入了指针输入消息的概念。这些消息来自各种输入设备,如触摸、笔或鼠标(在执行 EnableMouseInPointer
函数后)。它们也可以通过新的 InjectTouchInput
函数以编程方式“注入”。WM_POINTER*
消息在某些方面与 WM_TOUCH
类似,但提供了更多关于用户输入的信息。例如,您可以轻松区分鼠标输入和触摸输入。特殊的 WM_TOUCHHITTESTING
消息有助于确定最可能的目标等等。当然,它支持多点触控输入。但是,总的来说,与 WM_TOUCH
消息一样,您只有触摸点(或鼠标单击)的 (x, y) 坐标,以及有关指针是已添加、已移动还是已移除的信息。WM_POINTER*
消息是底层的东西,不会告诉我们任何关于触摸手势的信息。
在浏览 MSDN 时,我发现了一个关于新 API 的部分,该 API 使 Windows 8 应用程序能够通过提供手势检测和操作处理来支持多个并发交互。这就是 Interaction Context。它的文档仍然非常匮乏且含糊不清。我无法使用它,直到我找到了 Intel 网站上一篇出色的 文章 和示例。
Interaction Context 是一个与 WM_POINTER*
消息配合使用的 API。它接收来自指针的底层数据,然后执行一个回调函数,将关于触摸交互的高级信息作为参数传递给该函数。与 WM_TOUCH
消息中的 IManipulationProcessor
和 IInertiaProcessor
接口相比,使用 Interaction Context 函数要容易得多。您不再需要担心 COM 和事件接收器。甚至可以为该 API 创建托管包装器,并在 C# 代码中使用它。
灵活性和简洁性是新的触摸 API 的主要优势。它将触摸目标与窗口分开。例如,在 InteractionContextTest
示例中,您可以看到几个几何图形。它们不是窗口,只是可以自行绘制在 Form 上的对象。这些图形中的每一个以及背景都会创建并调整自己的交互上下文,因此,当您使用鼠标或触摸设备移动图形时,它们可以表现出各种行为。这些上下文是相互独立的。因此,例如,您可以用两只手同时移动两个图形。更进一步,您可以用触摸设备移动/缩放/旋转一个图形,同时用鼠标移动另一个图形。这在使用 WM_TOUCH
消息时几乎是不可能的。
此外,Interaction Context 的视觉反馈更一致。例如,如果您为某个上下文启用了“按住”行为(模拟右键单击),则添加到该上下文的第一个指针在一段时间后会在屏幕上显示一个正方形。不属于该上下文的指针将不会显示正方形,尽管所有指针都被传递给了同一个窗口。这意味着交互上下文可能会影响关联的指针,而不仅仅是从指针获取数据。
创建 Interaction Context
在示例中,我将所有互操作代码移至 Win32.cs 文件。您可以在那里找到传递给/来自 API 函数的许多枚举和结构的定义。这些函数也在此处定义为 DllImports
。有关 指针输入消息 和 Interaction Context 的更多信息,请参阅 MSDN。
需要与交互上下文关联的对象必须实现 IInteractionHandler
接口。它非常简单。
internal interface IInteractionHandler
{
void ProcessInteractionEvent(InteractionOutput output);
}
InteractionOutput
类包含从交互上下文传递到回调函数的数据:
internal class InteractionOutput
{
internal Win32.INTERACTION_CONTEXT_OUTPUT Data;
internal IntPtr InteractionContext { get; }
internal bool IsBegin();
internal bool IsInertia();
internal bool IsEnd();
// and a few infrastructure members
}
IInteractionHandler
接口的实现可以在示例中的 BaseHandler
类中找到。Figure
和 Background
类都派生自 BaseHandler
。回调函数在所有交互上下文之间共享。它是 Win32
静态类中基础设施的一部分。您可以在那里看到一些技巧,使其线程安全,并允许使用单个计时器为所有交互上下文处理惯性。
要创建交互上下文,我们必须调用 Win32.CreateInteractionContext
方法。它接受 IInteractionHandler
接口作为参数,以及当前的 SynchronizationContext
。
IntPtr _context;
public BaseHandler()
{
_context = Win32.CreateInteractionContext(this, SynchronizationContext.Current);
}
在对象的 Dispose
方法中,我们必须调用 Win32.DisposeInteractionContext
方法。
public void Dispose()
{
Win32.DisposeInteractionContext(_context);
_context = IntPtr.Zero;
}
在使用交互上下文之前,必须对其进行配置。例如,在 Background
类的构造函数中,我们使用以下代码来更新其配置。
Win32.INTERACTION_CONTEXT_CONFIGURATION[] cfg = new Win32.INTERACTION_CONTEXT_CONFIGURATION[]
{
new Win32.INTERACTION_CONTEXT_CONFIGURATION(Win32.INTERACTION.TAP,
Win32.INTERACTION_CONFIGURATION_FLAGS.TAP |
Win32.INTERACTION_CONFIGURATION_FLAGS.TAP_DOUBLE),
new Win32.INTERACTION_CONTEXT_CONFIGURATION(Win32.INTERACTION.SECONDARY_TAP,
Win32.INTERACTION_CONFIGURATION_FLAGS.SECONDARY_TAP),
new Win32.INTERACTION_CONTEXT_CONFIGURATION(Win32.INTERACTION.HOLD,
Win32.INTERACTION_CONFIGURATION_FLAGS.HOLD)
};
Win32.SetInteractionConfigurationInteractionContext(Context, cfg.Length, cfg);
背景不支持任何交互,除了轻触、双击和次级轻触(右键单击)。我们还启用了按住交互,为次级轻触交互提供更好的反馈。
在 Figure
类的构造函数中,我们根据传递给构造函数的参数启用各种操作。
Win32.INTERACTION_CONTEXT_CONFIGURATION[] cfg = new Win32.INTERACTION_CONTEXT_CONFIGURATION[]
{
new Win32.INTERACTION_CONTEXT_CONFIGURATION(Win32.INTERACTION.MANIPULATION,
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION |
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_SCALING |
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_ROTATION |
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_TRANSLATION_INERTIA |
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_ROTATION_INERTIA |
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_SCALING_INERTIA),
new Win32.INTERACTION_CONTEXT_CONFIGURATION(Win32.INTERACTION.TAP,
Win32.INTERACTION_CONFIGURATION_FLAGS.TAP |
Win32.INTERACTION_CONFIGURATION_FLAGS.TAP_DOUBLE)
};
if (!pivot)
{
cfg[0].Enable |=
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_TRANSLATION_X |
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_TRANSLATION_Y;
}
if (rails)
{
cfg[0].Enable |=
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_RAILS_X |
Win32.INTERACTION_CONFIGURATION_FLAGS.MANIPULATION_RAILS_Y;
}
Win32.SetInteractionConfigurationInteractionContext(Context, cfg.Length, cfg);
配置参数可以随时分配,但更改仅应用于新交互。
处理指针消息
在 Form 的 WndProc
方法中,我们应该处理这些消息。
WM_POINTERDOWN
WM_POINTERUP
WM_POINTERUPDATE
WM_POINTERCAPTURECHANGED
为了从窗口消息参数中获取指针 ID
及其 POINTER_INFO
,我们使用以下代码。
int pointerID = Win32.GET_POINTER_ID(m.WParam);
Win32.POINTER_INFO pi = new Win32.POINTER_INFO();
if (!Win32.GetPointerInfo(pointerID, ref pi))
{
Win32.CheckLastError();
}
当用户触摸屏幕或按下鼠标按钮时,会发生 WM_POINTERDOWN
消息,因此会添加一个新指针。在处理 WM_POINTERDOWN
消息时,我们需要进行命中测试,并确定包含新指针的图形。然后,我们应该使用 AddPointerInteractionContext
函数将指针添加到图形的交互上下文中。由于指针 ID
可以在图形的 ActivePointers
哈希集中轻松找到,因此该指针的所有后续消息都可以传递到同一个上下文,无需进行命中测试。
以下是处理 WM_POINTERDOWN
消息的简化代码版本。
case Win32.WM_POINTERDOWN:
{
Point pt = PointToClient(pi.PtPixelLocation.ToPoint());
foreach (Figure f in _figures)
{
if (f.HitTest(pt.X, pt.Y))
{
f.AddPointer(pointerID);
f.ProcessPointerFrames(pointerID, pi.FrameID);
break;
}
}
}
break;
Figure
类中的 HitTest
方法确定点是否包含在给定图形的边界内。该方法很简单但功能强大。它可以用于任何凸形,而不仅仅是矩形。AddPointer
方法将指针 ID
与交互上下文关联,并将 ID
添加到给定图形的 ActivePointers
哈希集中。ProcessPointerFrames
方法的代码如下。
int _lastFrameID = -1;
public void ProcessPointerFrames(int pointerID, int frameID)
{
if (_lastFrameID != frameID)
{
_lastFrameID = frameID;
int entriesCount = 0;
int pointerCount = 0;
if (!Win32.GetPointerFrameInfoHistory(pointerID, ref entriesCount,
ref pointerCount, IntPtr.Zero))
{
Win32.CheckLastError();
}
Win32.POINTER_INFO[] piArr = new Win32.POINTER_INFO[entriesCount * pointerCount];
if (!Win32.GetPointerFrameInfoHistory(pointerID, ref entriesCount, ref pointerCount, piArr))
{
Win32.CheckLastError();
}
IntPtr hr = Win32.ProcessPointerFramesInteractionContext(_context,
entriesCount, pointerCount, piArr);
if (Win32.FAILED(hr))
{
Debug.WriteLine("ProcessPointerFrames failed: " + Win32.GetMessageForHR(hr));
}
}
}
此方法获取指定指针的完整信息帧,并将此帧传递给交互上下文。
WM_POINTERUP
消息发生在指针被移除时。我们必须从 ActivePointers
哈希集中移除指针,并从交互上下文中移除。
case Win32.WM_POINTERUP:
foreach (Figure f in _figures)
{
if (f.ActivePointers.Contains(pointerID))
{
f.ProcessPointerFrames(pointerID, pi.FrameID);
f.RemovePointer(pointerID);
break;
}
}
break;
WM_POINTERUPDATE
消息通常表示指针位置已更改。处理很简单。
case Win32.WM_POINTERUPDATE:
foreach (Figure f in _figures)
{
if (f.ActivePointers.Contains(pointerID))
{
f.ProcessPointerFrames(pointerID, pi.FrameID);
break;
}
}
break;
当窗口即将失去捕获时,可能会发生 WM_POINTERCAPTURECHANGED
消息。如果是这样,我们会移除所有指针并停止交互上下文。
处理交互事件
最终,交互上下文的回调函数在派生自 BaseHandler
的类中执行 ProcessEvent
方法。使用 InteractionOutput
对象的 IsBegin()
、IsEnd()
和 IsInertia()
方法来确定交互处理的状态。您应该查看传递给 ProcessEvent
方法作为 output.Data
的 INTERACTION_CONTEXT_OUTPUT
结构定义。当前交互的类型可以从 Interaction
字段(例如 manipulation、tap、cross-slide 等)中获取。交互的源可以从 InputType
(例如 mouse、pen 或 touch)中获取。对于 manipulation、tap 和 cross-slide 交互,还有特殊的子结构。例如,它在 Background
的代码中使用:
internal override void ProcessEvent(InteractionOutput output)
{
if (output.Data.Interaction == Win32.INTERACTION.TAP)
{
if (output.Data.Tap.Count == 2)
{
foreach (Figure f in _form.Figures)
{
f.ResetPoints();
}
_form.Invalidate();
}
}
}
在 InteractionContextTest
示例中,您可以双击背景来重置所有图形的位置。此外,按住手势(或鼠标右键单击)会更改背景颜色。图形可以被移动(除了固定在原地的那个)、缩放、旋转和双击。固定在原地的图形可以用一个手指旋转。希望您会发现此示例很有用。
结论
Windows 8 引入了用于统一处理来自各种定位设备输入的 API。指针输入消息和 Interaction Context 函数在处理触摸输入时特别有用。您可以将 InteractionContextTest
示例作为您自己开发的基础。如果您不必考虑与 Windows 7 的向后兼容性,那么为 Windows 8 桌面应用程序添加触摸支持将变得很容易。