🎓 严老师 · C# / WPF 教学
← 全部课程 Day 27 Phase 4 第 11 课

📚 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(按"是否掌握"/"卡在哪"分段评估)。

📡 推送信息

教学日期
2026-08-27
所属阶段
Phase 4 第 11 课
消息条数
5 条(飞书 DM teacher-yan-bot 推送)
消息 ID
om_x100b67c3a060dca0b4cd5f2e34f4732om_x100b67c3a146b0b8b4a52c3885b3272om_x100b67c3beba28a4b2709687a7743edom_x100b67c3bfb6d8a4b2b6578f2e2a9bfom_x100b67c3bc7cc8a8b107de2ab42e70d
数据来源
feishu_via_memory_mid