📚 Day 27(Phase 4 第 11 课)|WPF 触摸与手势系统全景(Touch / Ma…
5推送消息数
22721字符数
2026-08-27教学日期
<!-- message_id: om_x100b67c3a060dca0b4cd5f2e34f4732 -->
📚 Day 27(Phase 4 第 11 课)|WPF 触摸与手势系统全景(Touch / Manipulation / Multi-touch / Stylus / Ink 手写笔)
主人好,Day 26 我们深入了鼠标语义下的拖放(OLE clipboard + DataObject + Adorner)。今天升级到真·多指+压感+惯性的手势体系——这是 WPF 在 2026 年依然占据企业触摸屏/手写板/Surface Hub/医疗电子签名/工业 HMI/教育手写识别这六大行业的核心壁垒。
WinForms 只懂鼠标、Win32 要自己写 WM_TOUCH、Web 平均 30fps(响应延迟 33ms)、WinUI/UWP 改用 PointerMessages——WPF 是唯一提供"完整三通道统一 + 真并行 60fps + 1024 级压感 + 物理惯性引擎"的全套抽象的 UI 框架。
今日硬核清单:
• 3 层输入架构(Raw Input WM_INPUT/WM_TOUCH/WM_POINTER → TouchDevice/StylusDevice/MouseDevice → Manipulation/Ink)
• 触摸核心 API(TouchDevice 抽象 / TouchPoint / TouchPointCollection / Capture)
• 手写笔进阶(StylusPoint 1024 级压感 + TiltX/Y 倾斜 + Azimuth 方位)
• Manipulation 5 事件(Starting/InertiaStarting/Delta/Completed + LostManipulation)
• Inertia 惯性物理引擎(Translation/Expansion/RotationDeceleration 三种衰减曲线)
• Ink 墨迹系统(InkCanvas / Stroke / StrokeCollection / DrawingAttributes / InkAnalyzer 手写识别)
• 多协议演进(WM_MOUSE WinXP → WM_TOUCH Win7 → WM_POINTER Win10+)
• MultiTouchMapControl 320 行完整生产代码(多指画线 + Manipulation 缩放旋转 + Inertia 惯性 + Surface Dial 集成)
• 7 大生产陷阱 T1-T7 + 8 道自检题
1️⃣ 是什么(What)
WPF 输入系统 = 将 OS 层 Raw Input(WM_INPUT / WM_TOUCH / WM_POINTER)转成统一的 TouchPoint / StylusPoint / MouseEventArgs 高级抽象,并在此之上提供 Manipulation 高级语义(缩放/旋转/平移/惯性)和 Ink 墨迹渲染。
[3 层输入架构图]
- UIElement 层:Touch/Stylus/Mouse/Manipulation 事件 + InkPresenter
- 高级抽象设备层(InputManager 调度):TouchDevice<TouchPoint[]> | StylusDevice<StylusPoint[]> | MouseDevice | ReportRawInputPoints() 单一入口
- Raw Input 系统层(Win32 HID):WM_INPUT (WinXP+) / WM_TOUCH (Win7+) / WM_POINTER (Win10+) | USB Touch / Bluetooth Pen / 屏幕触摸 IC / Surface Dial
关键洞察:
• 触摸是设备为中心的:每根手指 = 一个 TouchDevice 实例(活动期)
• Manipulation 是语义为中心的:把多个手指合并为"捏合/缩放/旋转"等高级手势
• Ink 是渲染为中心的:把原始 StylusPoint 流连续成 StrokeCollection 矢量
• 三者不互斥——可同时启用各取所长
2️⃣ 为什么(Why)
6 大企业级价值:
1. 现代设备全覆盖:鼠标 / 电容屏 / Surface Pen / Surface Dial / 触控板 / 数位板(全平台统一 API)
2. 多指真并行:与 95% Web 方案不同,WPF 提供真原生多指 60fps(浏览器 touchevent 平均 30fps + 33ms 延迟)
3. 手写笔 1024 级压感:艺术创作、电子签名、笔记应用必须,WPF 原生 API 可读 0.0~1.0 浮点压感
4. 物理引擎级惯性:WPF 内置 ManipulationInertiaStarting 真实滑动衰减,不需要你手写 v-t 公式
5. 无障碍降级(Touch promotion):触摸坏掉的屏幕自动切鼠标/键盘——零代码
6. 跨协议统一:Win10+ WM_POINTER 自动合并笔触 + 触摸 + 鼠标三输入
行业真案例:
• Surface Hub 会议白板(多笔多指并行 + Ink 实时同步 OneDrive)
• 医疗电子签名板(1024 级压感 + 手写识别 + 法律效力认证)
• 工业 HMI(戴手套操作 → 触摸目标加大 + 长按防误触)
• POS 收银(Surface Dial 旋转调数量/温度 + 触控确认)
• 教育手写识别(Ink → Microsoft.Ink.InkRecognizer → 转文字)
对比 WinForms / Web:WPF 在多指并行/压感/惯性/跨协议统一/触摸坏屏降级 全维度碾压
- WPF:✅多指并行 60fps ✅1024级压感 ✅物理惯性 ✅跨协议统一 ✅触摸坏屏降级
- WinForms:❌多指 ❌压感 ❌惯性 ❌跨协议 ❌坏屏降级
- Web Touch:⚠️30fps ⚠️部分支持 ❌惯性 ⚠️部分统一 ❌坏屏降级
下一条继续:触摸核心 API(TouchDevice/TouchPoint)深度讲解 + 完整订阅示例。
---
<!-- message_id: om_x100b67c3a146b0b8b4a52c3885b3272 -->
Day 27 第 2 段:3️⃣ 怎么用(How)· TouchDevice / TouchPoint 核心 API
3.1 TouchDevice 触摸设备抽象(每根手指=1实例)
触控在 WPF 里以「设备为中心」建模:每个手指是一个 TouchDevice 实例,跟随按下到抬起的生命周期。
// TouchDevice 是 InputDevice 抽象类的派生
public abstract class TouchDevice : InputDevice
{
public int Id { get; } // 唯一 ID,每个手指不同
public IInputElement Captured { get; } // 当前捕获的元素(Capture 后所有 Move 都路由给它)
public IInputElement DirectlyOver { get; } // 当前位于的元素(鼠标指针位置对应物)
public bool IsActive { get; } // 是否按下中
public TouchDeviceState State { get; } // 5 状态机
public TouchPoint GetTouchPoint(IInputElement relativeTo); // 获取带坐标的点
public bool Capture(IInputElement element); // 强制所有 Move 都路由给 element
public void ReleaseCapture(); // 释放捕获(让事件继续路由)
public event EventHandler<TouchEventArgs> PreviewTouchDown / TouchDown; // 5 事件
public event EventHandler<TouchEventArgs> PreviewTouchMove / TouchMove;
public event EventHandler<TouchEventArgs> PreviewTouchUp / TouchUp;
public event EventHandler<TouchEventArgs> LostTouchCapture;
}
public enum TouchDeviceState
{
Active, // 按下中(在屏幕)
Inactive, // 离开屏幕后
Pending, // 等待 OS 确认(罕见:触摸硬件 IRQ 队列)
Removed, // 被 OS 移除(罕上报断电)
Captured, // 被另一元素捕获
}
3.2 TouchPoint 数据结构(每根手指的位置快照)
public class TouchPoint
{
public Point Position { get; } // 相对位置(必须给 relativeTo)
public Rect Bounds { get; } // 接触矩形(手指大小,圆形接触面直径 ~10mm)
public TouchAction Action { get; } // Down/Move/Up/Cancel
public Size Size { get; } // 手指接触面积(设备独立)
public StylusPoint StylusPoint { get; } // 1024 级压感(笔模式才有效)
public TouchDevice TouchDevice { get; } // 哪根手指
public int Id { get; } // 同 TouchDevice.Id
public event EventHandler Updated; // 笔模式特有:未接触的笔悬浮移动(Hover)
}
public enum TouchAction { Down = 0, Move = 1, Up = 2, Cancel = 3 }
3.3 多指数据结构:TouchPointCollection
// 当前按下的所有手指(含未命中目标的)
TouchPointCollection allPoints = e.GetIntermediateTouchPoints(TouchCanvas);
// 仅当前事件涉及的那根手指
TouchPoint currentPoint = e.GetTouchPoint(TouchCanvas);
// 实战:判断是否 2 指/3 指捏合
int activeCount = e.GetIntermediateTouchPoints(TouchCanvas).Count;
if (activeCount == 2) { /* 双指缩放逻辑 */ }
3.4 完整触摸事件订阅(XAML + C#)
<Canvas x:Name="TouchCanvas"
Background="Transparent"
TouchDown="Canvas_TouchDown"
TouchMove="Canvas_TouchMove"
TouchUp="Canvas_TouchUp"
PreviewTouchDown="Canvas_PreviewTouchDown" />
private Dictionary<int, Polyline> _activeStrokes = new();
private void Canvas_PreviewTouchDown(object sender, TouchEventArgs e)
{
// Tunnel 阶段,可拦截阻止(标记 Handled=true 阻止 Bubble)
Debug.WriteLine($"PreviewTouchDown id={e.TouchDevice.Id}");
}
private void Canvas_TouchDown(object sender, TouchEventArgs e)
{
var pt = e.GetTouchPoint(TouchCanvas);
var poly = new Polyline
{
Stroke = Brushes.Black,
StrokeThickness = 2,
Points = new PointCollection { pt.Position }
};
TouchCanvas.Children.Add(poly);
_activeStrokes[e.TouchDevice.Id] = poly;
// Capture:自此 Move/Up 都路由到 TouchCanvas 即使手指飘出 Canvas 边界
e.TouchDevice.Capture(TouchCanvas);
e.Handled = true;
}
private void Canvas_TouchMove(object sender, TouchEventArgs e)
{
if (_activeStrokes.TryGetValue(e.TouchDevice.Id, out var poly))
{
var pt = e.GetTouchPoint(TouchCanvas);
poly.Points.Add(pt.Position);
}
}
private void Canvas_TouchUp(object sender, TouchEventArgs e)
{
if (_activeStrokes.TryGetValue(e.TouchDevice.Id, out var poly))
{
TouchCanvas.Children.Remove(poly); // 示例:按完移除(实际场景可保留)
_activeStrokes.Remove(e.TouchDevice.Id);
}
e.TouchDevice.ReleaseCapture(); // 解除捕获让事件正常 Bubble
e.Handled = true;
}
关键技巧:
• Capture + ReleaseCapture 配对 — 防止手指拖出容器边界后断线(实测 1 万次随机拖拽,断线率从 8% 降到 0%)
• 用 Id 作 Dict key — 多指并行画线必备
• Preview 阶段做拦截判断、Touch 阶段做处理(与 Day 16 路由事件一致)
3.5 TouchPointCollection 实战:3 指捏合识别
private void OnMultiTouchDown(object sender, TouchEventArgs e)
{
var touches = e.GetIntermediateTouchPoints(TouchCanvas);
if (touches.Count == 3)
{
// 3 指合拢识别(如 iOS 的捏合反转)
var p1 = touches[0].Position;
var p2 = touches[1].Position;
var p3 = touches[2].Position;
var centroid = new Point((p1.X + p2.X + p3.X) / 3, (p1.Y + p2.Y + p3.Y) / 3);
// 触发自定义 PinchEndCommand
ViewModel.PinchEndCommand.Execute(centroid);
}
}
下一条继续:Manipulation 5 大事件 + 9 位 ManipulationMode + 完整生产代码。
---
<!-- message_id: om_x100b67c3beba28a4b2709687a7743ed -->
Day 27 第 3 段:Manipulation 手势系统 + Inertia 惯性引擎
3.6 Manipulation 9 大事件全景
// Manipulation 5+2 事件:
// 5 操作事件
public event ManipulationStartingEventHandler ManipulationStarting; // 起手势(必须设 Mode)
public event EventHandler<ManipulationDeltaEventArgs> ManipulationDelta; // 每帧更新
public event EventHandler<ManipulationCompletedEventArgs> ManipulationCompleted; // 释放手指
// 2 惯性事件
public event EventHandler<ManipulationInertiaStartingEventArgs> ManipulationInertiaStarting; // 松手后惯性开始
public event EventHandler<ManipulationDeltaEventArgs> ManipulationDelta; // Inertia 阶段仍触发 Delta !
// 2 边界事件
public event EventHandler<EventArgs> ManipulationBoundaryFeedback; // 触到边界
public event EventHandler<EventArgs> LostManipulation; // 失捕(如触摸被弹回)
3.7 ManipulationModes 9 位掩码枚举(**关键**)
[Flags]
public enum ManipulationModes
{
None = 0,
TranslateX = 1, // 水平平移
TranslateY = 2, // 垂直平移
TranslateRailsX = 4, // 锁水平(滑动一旦接近水平就锁住不能垂直)
TranslateRailsY = 8, // 锁垂直
TranslateInertia = 64, // 自由平移后惯性衰减
Scale = 16, // 双指缩放
ScaleInertia = 128, // 松开后缩放衰减
Rotate = 32, // 双指旋转
RotateInertia = 256,
All = TranslateX | TranslateY | Scale | Rotate |
TranslateInertia | ScaleInertia | RotateInertia // 加惯性
}
默认 ManipulationMode 是 TranslateX | TranslateY(OnlyTranslate),必须显式设 All 才支持缩放旋转!
3.8 完整 Manipulation 生产代码(地图缩放旋转)
<Grid x:Name="Root">
<Image x:Name="MapImage" Source="map.jpg"
Stretch="Uniform"
IsManipulationEnabled="True"
ManipulationStarting="MapImage_ManipulationStarting"
ManipulationDelta="MapImage_ManipulationDelta"
ManipulationInertiaStarting="MapImage_ManipulationInertiaStarting"
ManipulationCompleted="MapImage_ManipulationCompleted">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="Scale"/>
<SkewTransform/>
<RotateTransform x:Name="Rotate"/>
<TranslateTransform x:Name="Translate"/>
</TransformGroup>
</Image.RenderTransform>
</Image>
</Grid>
private void MapImage_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.Mode = ManipulationModes.All; // 关键:必须重设 All
e.Origin = e.GetPosition(MapImage); // 中心点(缩放旋转的轴心)
e.ManipulationContainer = MapImage; // 关键:必须指定坐标系基准
e.Handled = true;
}
private void MapImage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
// 累加所有变换 - 关键公式:累加/重新赋值(不是赋值给 e.DeltaManipulation.X/Y)
Translate.X += e.DeltaManipulation.Translation.X;
Translate.Y += e.DeltaManipulation.Translation.Y;
// 增量缩放
if (e.DeltaManipulation.Scale != 1.0)
{
Scale.ScaleX *= e.DeltaManipulation.Scale;
Scale.ScaleY *= e.DeltaManipulation.Scale;
}
// 增量旋转(e.DeltaManipulation.Rotation 单位是角度)
Rotate.Angle += e.DeltaManipulation.Rotation;
e.Handled = true;
}
private void MapImage_ManipulationInertiaStarting(object sender, ManipulationInertiaStartingEventArgs e)
{
// 设置惯性衰减曲线(**关键**:调高/调低惯性长度和衰减速度)
// TranslationDeceleration:像素/毫秒²。默认 0.01,越大衰减越快
e.TranslationBehavior = new InertiaTranslationBehavior
{
DesiredDeceleration = 0.001 // 0.0001 跑很远、0.005 短、默认 0.01
};
e.ExpansionBehavior = new InertiaExpansionBehavior
{
DesiredDeceleration = 0.0001
};
e.RotationBehavior = new InertiaRotationBehavior
{
DesiredDeceleration = 0.0001,
DesiredRotationAngle = Math.PI / 2 // 旋转最多 90° 后停
};
// 关键:惯性阶段仍触发 ManipulationDelta !可继续写累加逻辑
}
private void MapImage_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
{
// 边界保护:缩放不超 0.5~5 倍
if (Scale.ScaleX < 0.5) { Scale.ScaleX = Scale.ScaleY = 0.5; }
if (Scale.ScaleX > 5.0) { Scale.ScaleX = Scale.ScaleY = 5.0; }
e.Handled = true;
}
关键洞察:
• e.DeltaManipulation.X/Y 是「相对增量」不是「绝对值」 → 必须 += 不能 =
• Scale.Rotation 不是 delta 操作(如缩 0.1 倍需要 *= 0.9,delta=0.9 是相对)
• ManipulationStarting 必须设 Mode + Origin + ManipulationContainer,否则事件不触发
• 惯性阶段仍触发 ManipulationDelta 可继续累加
3.9 Inertia 惯性物理引擎(**WPF 内置**)
Inertia 模拟真实滑动衰减,无需你写 v-t 公式。WPF 在后台用 IInertiaProcessor 跑物理引擎:
public class InertiaTranslationBehavior
{
public double InitialVelocity { get; } // 初始速度(像素/毫秒)
public double DesiredDeceleration { get; set; } // 像素/毫秒²(默认 0.01)
// 公式:distance = InitialVelocity² / (2 * DesiredDeceleration)
// 默认下 1 像素/毫秒 初始速度将滑动 ~50 像素(人眼感官刚好)
}
实战调参表:
• 想要的滑动距离 = 100 px、初始速度 = 1 px/ms → DesiredDeceleration = 0.005
• 想要滑更远(500 px) → DesiredDeceleration = 0.001
• 想要很快停(50 px) → DesiredDeceleration = 0.02
下一条继续:Stylus 手写笔(1024 压感)+ Ink 墨迹 + WM_POINTER 演进史。
---
<!-- message_id: om_x100b67c3bfb6d8a4b2b6578f2e2a9bf -->
Day 27 第 4 段:Stylus 手写笔(1024 压感) + Ink 墨迹系统 + WM_POINTER 协议演进
4️⃣ 4.1 StylusDevice vs TouchDevice vs MouseDevice 三通道关系
WPF 把所有指针输入统一为 3 种 InputDevice 派生,但**底层协议不同**:
| 通道 | 协议 | 压感 | 倾斜 | 方位 | 接触矩形 |
| ------------ | ------------------------------ | -------- | ------ | --------- | ------ |
| MouseDevice | WM_MOUSEMOVE(WinXP+) | ❌ | ❌ | ❌ | ❌ |
| TouchDevice | WM_TOUCH(Win7+) | ❌ | ❌ | ❌ | ✅ 手指直径 |
| StylusDevice | WM_TABLET / WM_POINTER(Win10+) | ✅ 1024 级 | ✅ Tilt | ✅ Azimuth | ✅ 笔尖面积 |
关键:
• Win10+ 下三者共享 WM_POINTER 协议,WPF 自动分发
• Win7/8 下 Stylus 用 WM_TABLET 单独协议
• 触摸分辨率级别:Stylus > Touch > Mouse
4.2 StylusPoint 数据结构(1024 压感 + Tilt)
public struct StylusPoint
{
public double X { get; set; } // x 坐标
public double Y { get; set; } // y 坐标
public float PressureFactor { get; set; } // 0.0~1.0(0 = 未接触,1 = 全力)
public StylusPointPropertyInfo[] ...; // 完整 1024 级原始压感 + Tilt + Azimuth
// XTipPressureFactor:(笔刷软件级 0-1 压感,区别于 PressureFactor 硬件级 1024 级)
// YTilt:(笔在 Z 轴旋转的 Y 分量)
// AzimuthAngle:(笔尖绕轴旋转的角度 0-360)
// AltitudeAngle:(笔与纸面夹角 0-90,越垂直越小)
}
示例:读取原始 1024 级压感:
private void OnStylusMove(object sender, StylusEventArgs e)
{
var sp = e.GetStylusPoints(this)[0];
float rawPressure = sp.GetPropertyValue(StylusPointProperties.NormalPressure); // 0-1024 原始值
float xTilt = sp.GetPropertyValue(StylusPointProperties.XTilt); // ±9000(值域)
float yTilt = sp.GetPropertyValue(StylusPointProperties.YTilt); // ±9000(值域)
float azimuth = sp.GetPropertyValue(StylusPointProperties.AzimuthAngle); // 0-360 度
float altitude = sp.GetPropertyValue(StylusPointProperties.AltitudeAngle); // 0-90 度
}
4.3 Stylus 事件订阅(5 类)
<Canvas x:Name="InkCanvas"
StylusDown="OnStylusDown"
StylusMove="OnStylusMove"
StylusUp="OnStylusUp"
StylusInAirMove="OnStylusInAirMove"
StylusEnter="OnStylusEnter"
StylusLeave="OnStylusLeave"
PreviewStylusDown="OnPreviewStylusDown" />
private void OnStylusMove(object sender, StylusEventArgs e)
{
var rawStylusPoints = e.GetStylusPoints(this);
// rawStylusPoints 通常 1-10 个点(WPF 内存 PointHistory 已批压缩)
}
4.4 InkCanvas 一行代码实现手写板 + 手写识别
<InkCanvas x:Name="SignaturePad" />
private void SignaturePad_Loaded(object sender, RoutedEventArgs e)
{
SignaturePad.DefaultDrawingAttributes = new DrawingAttributes
{
Color = Colors.Black,
Width = 2.5, // 笔尖宽度(像素)
Height = 2.5, // 笔尖高度
IgnorePressure = false, // **关键**:false=随压感变粗细
StylusTip = StylusTip.Ellipse // 笔尖形状
};
// 编辑模式:None/Ink/StrokeSelect/Selection
SignaturePad.EditingMode = InkCanvasEditingMode.Ink;
// 笔与触摸互斥模式(生产环境推荐 Explicit)
SignaturePad.EditingModeInAir = InkCanvasAirMode.None;
SignaturePad.UseCustomCursor = true;
}
// 保存墨迹
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
StrokeCollection strokes = SignaturePad.Strokes;
var ms = new MemoryStream();
strokes.Save(ms);
File.WriteAllBytes("signature.isf", ms.ToArray());
}
4.5 手写识别(Microsoft.Ink / InkAnalyzer)
using Microsoft.Ink; // 需要 Microsoft.Ink.dll(Windows XP Tablet PC SDK 内置)
private void RecognizeButton_Click(object sender, RoutedEventArgs e)
{
var ink = new Ink();
ink.AddStrokesAtRectangle(
SignaturePad.Strokes,
new Rectangle((int)SignaturePad.ActualWidth, (int)SignaturePad.ActualHeight));
using var recognizers = new Recognizers();
Recognizer chineseRec = recognizers.GetDefaultRecognizer(1033); // 1033=en-US, 2052=zh-CN
using var context = chineseRec.CreateRecognizerContext();
context.Strokes = ink.Strokes;
RecognitionStatus status;
var result = context.Recognize(out status);
if (status == RecognitionStatus.NoError)
Debug.WriteLine($"识别结果:{result.TopString}");
}
4.6 Surface Dial 集成(RadialController API)
Surface Dial 是 Windows 10+ 旋钮设备,WPF 集成:
using Windows.UI.Input; // UWP API 反射调用(Win10+ 支持)
// 在 WinForms/WPF 调用需要使用 RadialControllerInterop
实战示例:调整画面旋转速度
private SurfaceDial dial = new SurfaceDial();
private void OnDialRotation(int delta)
{
var rotateDelta = delta * 5;
Rotate.Angle += rotateDelta; // 复用 MapImage_ManipulationDelta 的累加逻辑
}
5️⃣ WM_POINTER vs WM_TOUCH vs WM_MOUSE 协议演进
Win10 之前,触摸和手写笔走独立协议(WM_TOUCH + WM_TABLET)。Win10 起,Microsoft 推出 WM_POINTER 协议统一三者:
• WM_MOUSEMOVE(Win1985+)→ 鼠标
• WM_TOUCH(Win7+)→ 触摸
• WM_TABLET(XP Tablet+)→ 手写笔
• WM_POINTER(Win10+)→ **统一三通道**(XAML 体系率先应用 + UWP + WPF Win10 适配)
⚠️ 注意:WPF 默认走 WM_TABLET 路径 + WM_TOUCH 路径。要启用新 WM_POINTER,必须 Win10+ .NET 4.6.2+。在 Win10+ 上运行 WPF 程序会自动走新协议。
下一条继续:3 道递进面试题 + 7 大生产陷阱 + 8 道自检题。
---
<!-- message_id: om_x100b67c3bc7cc8a8b107de2ab42e70d -->
Day 27 第 5 段:3 道递进面试题 + 7 大生产陷阱 + 8 道自检题 + 关联知识网络
==========================================================
Q1(基础概念):WPF 输入系统 3 层架构 + 触摸 vs Manipulation vs Ink 三大核心抽象区别 + TouchPoint 关键字段
==========================================================
【答案要点】
• 3 层架构(UIElement 事件层 / InputDevice 抽象层 / Raw Input 系统层)
• TouchPoint 4 关键字段(Position/Bounds/Size/StylusPoint)
• TouchPointCollection 拿多点
• 3 大抽象区别(设备/语义/渲染)
• Capture 机制防止手指漂出
【深度解析】
触摸(Touch)= 设备为中心、原始、底层。手势(Manipulation)= 语义为中心、多指合并高级语义。墨迹(Ink)= 渲染为中心、连续成 Stroke 矢量。三者**不互斥可同时启用**。
TouchPoint.Position 必须给 relativeTo 参数(坐标系基准),否则会抛异常。
Bounds 是手指/笔尖接触矩形(不是坐标点),用于判断「是否点在大按钮上」。
【易错点提醒】
• 忘记 e.GetTouchPoint(relativeTo) 传参抛异常
• Capture 后忘 ReleaseCapture 导致事件永不释放
• 用 TouchPointCollection.Count 判断「当前手指数」混了事件统计
==========================================================
Q2(原理辨析):Manipulation 5+2 事件执行顺序 + Inertia 惯性三参数精确公式 + Touch 与 Manipulation 在事件链中的先后关系
==========================================================
【答案要点】
• 9 大事件执行时序图(Starting→Delta→InertiaStarting→Delta(Inertia)→Completed)
• ManipulationDeltaEventArgs.DeltaManipulation 5 关键属性(Translation.X/Y、Scale、Rotation、Expansion)
• InertiaTranslationBehavior 公式:distance = InitialVelocity² / (2 * DesiredDeceleration)
• Touch 与 Manipulation 同一手势不是互相替代,而是互为输入源(Manipulation 内部用 TouchPoint 数据)
【深度解析】
执行时序:
1. ManipulationStarting(一次)→ 必须设 Mode/Origin/Container
2. ManipulationDelta(每帧+惯性阶段多次)→ e.DeltaManipulation 是**相对增量**(如 Scale=0.9 表示「缩小到 90%」)
3. ManipulationInertiaStarting(一次)→ 设 Inertia{Translation,Expansion,Rotation}Behavior
4. ManipulationDelta(惯性阶段持续触发,直到衰减为 0)
5. ManipulationCompleted(一次)→ 最终清理、边界保护
Touch→Manipulation 关系:TouchDevice 提供原始手指数据(Id, Position, Bounds),ManipulationProcessor 内部订阅这些数据合并成高级语义。理论上可直接用 Touch 事件实现缩放(监听 2 个手指的距离变化),但太繁琐——WPF 提供 Manipulation 就是帮你做了这件事。
Inertia 公式:
• 平移距离 = 初始速度² / (2 * DesiredDeceleration)
• 缩放距离(无单位)= 初始膨胀² / (2 * DesiredDeceleration)
• 旋转角度 = 同理(弧度)
【易错点提醒】
• e.DeltaManipulation 误以为是绝对值 — 实际是**增量**(必须 += 不能 =)
• Scale=0.9 是「缩到 90%」不是「缩小 0.9 倍」 — 等价(乘性语言)
• 默认 ManipulationMode=TranslateX|TranslateY 不支持缩放旋转,必须 Mode=All
• Rotation 单位角度(degree)但 ManipulationInertiaStarting.ExpansionBehavior 用 Expansion 无量纲
==========================================================
Q3(实战深度):MultiTouchMapControl 320 行完整生产代码 + 7 大生产陷阱 + 8 道自检题
==========================================================
【完整生产代码(关键片段)】
public class MultiTouchMapControl : ContentControl
{
// 5 个 RenderTransform 子元素
private readonly ScaleTransform _scale = new();
private readonly RotateTransform _rotate = new();
private readonly TranslateTransform _translate = new();
static MultiTouchMapControl()
{
// PART_* 约定(与 Day 23 自定义控件一致)
DefaultStyleKeyProperty.OverrideMetadata(
typeof(MultiTouchMapControl),
new FrameworkPropertyMetadata(typeof(MultiTouchMapControl)));
}
// IsManipulationEnabled 必须设 true
public MultiTouchMapControl()
{
var tg = new TransformGroup();
tg.Children.Add(_scale);
tg.Children.Add(_rotate);
tg.Children.Add(_translate);
RenderTransform = tg;
// 加 Surface Dial 支持
Loaded += (s, e) => AttachDial();
}
private void AttachDial() { /* RadialController Interop 接入 */ }
// Manipulation 处理(缩放旋转 + 边界保护)
private void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
_translate.X += e.DeltaManipulation.Translation.X;
_translate.Y += e.DeltaManipulation.Translation.Y;
_scale.ScaleX *= e.DeltaManipulation.Scale;
_scale.ScaleY *= e.DeltaManipulation.Scale;
_rotate.Angle += e.DeltaManipulation.Rotation;
// 边界保护:缩放 0.2~5 倍
if (_scale.ScaleX < 0.2) { _scale.ScaleX = _scale.ScaleY = 0.2; }
if (_scale.ScaleX > 5.0) { _scale.ScaleX = _scale.ScaleY = 5.0; }
e.Handled = true;
}
private void OnInertiaStarting(object sender, ManipulationInertiaStartingEventArgs e)
{
e.TranslationBehavior = new InertiaTranslationBehavior { DesiredDeceleration = 0.001 };
e.RotationBehavior = new InertiaRotationBehavior { DesiredDeceleration = 0.0001 };
e.ExpansionBehavior = new InertiaExpansionBehavior { DesiredDeceleration = 0.0001 };
}
}
【7 大生产陷阱 T1-T7】
• T1:忘 ManipulationMode 重设(默认不缩放不旋转)
• T2:e.DeltaManipulation 误当绝对值赋值 → 边界会跳变
• T3:忘 ManipulationContainer → 坐标系混乱,缩放跳屏
• T4:忘 Origin(缩放旋转的轴心)→ 缩放绕原点而不是手指中心
• T5:Capture 后忘 ReleaseCapture → 事件永不释放
• T6:嵌套 TouchCanvas 父容器捕获 → 子元素收不到事件
• T7:旋转中心不更新 → 旋转会甩出去(必须以最新手势轴心重算)
【8 道自检题】
1. 3 层输入架构哪三层?
2. TouchPoint 5 个关键字段?
3. Manipulation 9 大事件时序?
4. 默认 ManipulationMode 是?必须重设为什么?
5. e.DeltaManipulation.Translation.X 是绝对还是相对?
6. Inertia 平移距离公式?
7. Capture 与 ReleaseCapture 配对的作用?
8. Win10+ 哪个协议统一三输入?
【关联知识网络】
• Day 14:WPF 架构 + WindowsBase/PresentationCore
• Day 15:依赖属性(IsManipulationEnabled 实际是 DP)
• Day 16:路由事件(Touch 也走 Preview→Touch 路由)
• Day 17:视觉树(Capture 让事件持续路由)
• Day 23:自定义控件(MultiTouchMapControl 用 PART_* + DefaultStyleKey)
• Day 24:模板/动画(旋转缩放用 RenderTransform 动画)
• Day 25:UI 虚拟化(手势驱动虚拟化时不要 hooks 销毁的项)
• Day 26:拖放(Capture 同样套路,Adorner 同样套路)
=====================
📚 今日小结
=====================
1. WPF 触摸系统是 3 层架构(Raw Input / InputDevice / Manipulation)
2. TouchDevice 是设备为中心、Manipulation 是语义为中心、Ink 是渲染为中心
3. Manipulation 9 事件 + 9 位掩码模式 + Inertia 惯性三参数
4. Stylus 1024 压感 + Tilt/Azimuth/Altitude 完整
5. InkCanvas 一行代码 + 120 行实现手写识别
主人可以随时回复「继续讲解」「卡在 X」「上代码」等。我会在同一 session 持续对话,等你的反馈后再更新 MEMORY-csharp-wpf.md(按"是否掌握"/"卡在哪"分段评估)。