到目前为止,使用触摸事件处理图片与使用鼠标功能并没有太大区别。下面我们将:
• 添加使用多个手指操作图片的能力
• 同时平移、缩放和旋转图片
• 同时操作多张图片
我们已经知道如何将正确的事件分派给相应的 PictureTracker,但我们还不知道如何决定在发生多个事件之后需要采取的操作。这正是 Windows 7 多点触摸机制的用武之地。它拥有一个操作处理器来使用触摸 ID 事件并生成合适的操作事件。您只需实例化一个操作处理器,注册其事件,并为它提供触摸 ID + 位置事件对。
操作处理器是一个 COM 对象。要在 .NET 中使用它,可以使用 Windows 7 Integration Library 示例。ManipulationProcessor .NET 包装器类构造函数获得一个枚举值,该值告诉它要报告哪些操作。在我们的示例中,我们希望报告所有操作。该处理器有 3 个事件:ManipulationStarted、ManipulationCompleted 和 ManipulationDelta。ManipulationDelta 是我们所关注的事件。它提供了平移、旋转和缩放的偏移量。
1. 更改整个 PictureTracker 类。
- class PictureTracker
- {
- private readonly ManipulationProcessor _processor =
- new ManipulationProcessor(ProcessorManipulations.ALL);
- public PictureTracker()
- {
- _processor.ManipulationStarted += (s, e) =>
- {
- System.Diagnostics.Trace.WriteLine("Manipulation has started: " + Picture.ImagePath);
- };
- _processor.ManipulationCompleted += (s, e) =>
- {
- System.Diagnostics.Trace.WriteLine("Manipulation has completed: " + Picture.ImagePath);
- };
- _processor.ManipulationDelta += ProcessManipulationDelta;
- }
- public Picture Picture { get; set; }
- public void ProcessDown(int id, Point location)
- {
- _processor.ProcessDown((uint)id, location.ToDrawingPointF());
- }
- public void ProcessMove(int id, Point location)
- {
- _processor.ProcessMove((uint)id, location.ToDrawingPointF());
- }
- public void ProcessUp(int id, Point location)
- {
- _processor.ProcessUp((uint)id, location.ToDrawingPointF());
- }
- //Update picture state
- private void ProcessManipulationDelta(object sender, ManipulationDeltaEventArgs e)
- {
- if (Picture == null)
- return;
- Picture.X += e.TranslationDelta.Width;
- Picture.Y += e.TranslationDelta.Height;
- Picture.Angle += e.RotationDelta * 180 / Math.PI;
- Picture.ScaleX *= e.ScaleDelta;
- Picture.ScaleY *= e.ScaleDelta;
- }
- }
2. 将以下命名空间指令添加到 PictureTracker 类中:
- using Windows7.Multitouch.Manipulation;
- using Windows7.Multitouch.WPF;
- Visual Basic
- Imports Windows7.Multitouch.Manipulation
- Imports Windows7.Multitouch.WPF
注意: 通过添加此命名空间,可以使用 ManipulatorProcessor 类和 System.Windows.Point 扩展方法 ToDrawingPointF。
3. 我们实例化了一个新的 ManipulationProcessor,注册了事件处理器,而且最重要的是,通过更新图片用户控件处理了 ManipulationDelta 事件。现在我们需要对 PictureTrackerManager 事件处理代码稍作修改,并转发触摸 ID 和触摸位置。ManipulationProcessor 需要将触摸 ID 作为操作流程的输入。更改 PictureTrackerManager 中的以下代码:
- public void ProcessDown(object sender, StylusEventArgs args)
- {
- Point location = args.GetPosition(_canvas);
- PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id, location);
- if (pictureTracker == null)
- return;
- pictureTracker.ProcessDown(args.StylusDevice.Id, location);
- }
- public void ProcessUp(object sender, StylusEventArgs args)
- {
- Point location = args.GetPosition(_canvas);
- PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id);
- if (pictureTracker == null)
- return;
- pictureTracker.ProcessUp(args.StylusDevice.Id, location);
- _pictureTrackerMap.Remove(args.StylusDevice.Id);
- }
- public void ProcessMove(object sender, StylusEventArgs args)
- {
- PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id);
- if (pictureTracker == null)
- return;
- Point location = args.GetPosition(_canvas);
- pictureTracker.ProcessMove(args.StylusDevice.Id, location);
- }
4. 编译并运行代码。尝试同时操作多张图片。
任务 6 – 添加 PictureTracker 缓存
当用户首次触摸一张图片时,应用程序创建一个新 PictureTracker 实例,该实例然后创建 ManipulationProcessor COM 对象。只要用户移开触摸该图片的最后一个指头(触摸 ID),PictureTracker 实例就会被当作垃圾收集,进而释放底层 COM 对象。分析常见的应用程序使用情形就会发现,只有少数图片可能被同时操作。据此可以得出结论:我们需要 PictureTracker 实例的一个缓存。该缓存将包含空闲的 PictureTracker 实例。当(发生 ProcessDown 事件时)需要新 PictureTracker 实例时,我们将首先尝试从缓存拉取实例,只有当缓存为空时才生成新实例。当完成对图片的操作时,我们将 PictureTracker 实例移入缓存。因为 ManipulationCompleted 是一个 ManipulationProcessor 事件,所以我们将要求 PictureTracker 处理该事件并将其转发给 PictureTrackerManager。这需要一个从 PictureTracker 到它的 PictureTrackerManager 的新引用(我们使用构造函数来传递该引用)。
1. 将堆栈数据成员添加到 PictureTrackerManager 类的开头:
- class PictureTrackerManager
- {
- //Cache for re-use of picture trackers
- private readonly Stack<PictureTracker> _pictureTrackers = new Stack<PictureTracker>();
- ...
2. 更改 GetPictureTracker() 函数。我们需要使用缓存,还需要将此引用传递给 PictureTracker 构造函数:
- private PictureTracker GetPictureTracker(int touchId, Point location)
- {
- ...
- //First time
- if (pictureTracker == null)
- {
- //take from stack
- if (_pictureTrackers.Count > 0)
- pictureTracker = _pictureTrackers.Pop();
- else //create new
- pictureTracker = new PictureTracker(this);
- pictureTracker.Picture = picture;
- BringPictureToFront(picture);
- }
- ...
- }
3. 添加一个逻辑,以在操作完成时将 PictureTracker 实例推回堆栈中。将以下代码粘贴到 PictureTrackerManager 类中。
- C#
- //Manipulation is completed, we can reuse the object
- public void Completed(PictureTracker pictureTracker)
- {
- pictureTracker.Picture = null;
- _pictureTrackers.Push(pictureTracker);
- }
4. 现在需要更改 PictureTracker 类,使其适应 PictureTrackerManager 中的代码更改。
a. 将 PictureTrackerManager 实例获取到构造函数中,然后存储它。
- class PictureTracker
- {
- private readonly ManipulationProcessor _processor =
- new ManipulationProcessor(ProcessorManipulations.ALL);
- private readonly PictureTrackerManager _pictureTrackerManager;
- public PictureTracker(PictureTrackerManager pictureTrackerManager)
- {
- _pictureTrackerManager = pictureTrackerManager;
- ...
b. 在 ManipulationCompleted 事件中调用 PictureTrackerManager.Completed 函数:
- public PictureTracker(PictureTrackerManager pictureTrackerManager)
- {
- _pictureTrackerManager = pictureTrackerManager;
- _processor.ManipulationCompleted += (s, e) =>
- {
- System.Diagnostics.Trace.WriteLine("Manipulation has completed: " + Picture.ImagePath);
- _pictureTrackerManager.Completed(this);
- };
- ...
5. 编译并运行!
添加惯性
只剩最后一项任务了。使用缩放、平移和旋转操作可以提供一种自然的用户体验。在实际生活中,当推动一个物体,然后松开手时,该物体会继续移动,直到因为无法克服摩擦力而停止。可以使用 Inertia 让我们的图片对象拥有相同的行为。Windows 7 多点触摸子系统提供了一个 InertiaProcessor COM 对象。InertiaProcessor 可以发起与 ManipulationProcessor 相同的操作事件。Windows 7 Integration Library 示例提供了一个包装器,它将操作处理器和惯性处理器捆绑在一起。ManipulationInertiaProcessor 可以替代 ManipulationProcessor 并提供额外的 InertiaProcessor 属性来公开 InertiaProcessor 功能。要发起更多事件,ManipulationInertiaProcessor 需要一个计时器。为了克服线程的 UI 相似性问题,我们最好拥有一个基于 GUI 的计时器。Windows 7 Integration Library 可以为我们创建这样的计时器。
当用户的最后一个手指离开图片对象时,ManipulationInertiaProcessor 会发起 OnBeforeInertia 事件。在这里设置 Inertia 开始参数。可以选择一个默认的开始速度,或者跟踪当前的对象速度并从中提取出速度数字。
1. 我们想要跟踪对象的平移、旋转和缩放速度。将以下类添加到 PictureTracker 类中:
- //Keep track of object velocities
- private class InertiaParam
- {
- public VectorF InitialVelocity { get; set; }
- public float InitialAngularVelocity { get; set; }
- public float InitialExpansionVelocity { get; set; }
- public System.Diagnostics.Stopwatch _stopwatch = new System.Diagnostics.Stopwatch();
- public void Reset()
- {
- InitialVelocity = new VectorF(0, 0);
- InitialAngularVelocity = 0;
- InitialExpansionVelocity = 0;
- _stopwatch.Reset();
- _stopwatch.Start();
- }
- public void Stop()
- {
- _stopwatch.Stop();
- }
- //update velocities, velocity = distance/time
- public void Update(ManipulationDeltaEventArgs e, float history)
- {
- float elappsedMS = (float)_stopwatch.ElapsedMilliseconds;
- if (elappsedMS == 0)
- elappsedMS = 1;
- InitialVelocity = InitialVelocity * history + ((VectorF)e.TranslationDelta * (1F - history)) / elappsedMS;
- InitialAngularVelocity = InitialAngularVelocity * history + (e.RotationDelta * (1F - history)) / elappsedMS;
- InitialExpansionVelocity = InitialExpansionVelocity * history + (e.ExpansionDelta * (1F - history)) / elappsedMS;
- _stopwatch.Reset();
- _stopwatch.Start();
- }
- }
2. 将 OnBeforeInertia() 事件处理程序添加到 PictureTracker 类中:
- //Fingers removed, start inertia
- void OnBeforeInertia(object sender, BeforeInertiaEventArgs e)
- {
- //Tell the tracker manager that the user removed the fingers
- _pictureTrackerManager.InInertia(this);
- _processor.InertiaProcessor.InertiaTimerInterval = 15;
- _processor.InertiaProcessor.MaxInertiaSteps = 500;
- _processor.InertiaProcessor.InitialVelocity = _inertiaParam.InitialVelocity;
- _processor.InertiaProcessor.DesiredDisplacement = _inertiaParam.InitialVelocity.Magnitude * 250;
- _processor.InertiaProcessor.InitialAngularVelocity = _inertiaParam.InitialAngularVelocity * 20F / (float)Math.PI;
- _processor.InertiaProcessor.DesiredRotation = Math.Abs(_inertiaParam.InitialAngularVelocity *
- _processor.InertiaProcessor.InertiaTimerInterval * 540F / (float)Math.PI);
- _processor.InertiaProcessor.InitialExpansionVelocity = _inertiaParam.InitialExpansionVelocity * 15;
- _processor.InertiaProcessor.DesiredExpansion = Math.Abs(_inertiaParam.InitialExpansionVelocity * 4F);
- }
3. 更改 PictureTracker 类,创建 ManipulationInertiaProcessor 并注册 OnBeforeInertia 事件:
- /// <summary>
- /// Track a single picture
- /// </summary>
- class PictureTracker
- {
- ...
- //Calculate the Inertia start velocity
- private readonly InertiaParam _inertiaParam = new InertiaParam();
- private readonly ManipulationInertiaProcessor _processor = new ManipulationInertiaProcessor(ProcessorManipulations.ALL, Factory.CreateTimer());
- public PictureTracker(PictureTrackerManager pictureTrackerManager)
- {
- _pictureTrackerManager = pictureTrackerManager;
- //Start inertia velocity calculations
- _processor.ManipulationStarted += (s, e) =>
- {
- _inertiaParam.Reset();
- };
- //All completed, inform the tracker manager that the current tracker
- //can be reused
- _processor.ManipulationCompleted += (s, e) =>
- {
- _inertiaParam.Stop();
- pictureTrackerManager.Completed(this);
- };
- _processor.ManipulationDelta += ProcessManipulationDelta;
- _processor.BeforeInertia += OnBeforeInertia;
- }
- ...
4. 我们还需要更改 PictureTrackerManager。在新的条件下,图片可能由惯性处理器使用,即使没有手指在触摸该对象。我们需要在操作完成时立即从映射中删除触摸 ID,但是只有在惯性处理器使图片完全停止时,我们才能够重用 PictureTracker 对象。将 InInertia() 函数添加到 PictureTrackerManager 类中:
- //We remove the touchID from the tracking map since the fingers are
- //no longer touching the picture
- public void InInertia(PictureTracker pictureTracker)
- {
- //remove all touch id from the map
- foreach (int id in
- (from KeyValuePair<int, PictureTracker> entry in _pictureTrackerMap
- where entry.Value == pictureTracker
- select entry.Key).ToList())
- {
- _pictureTrackerMap.Remove(id);
- }
- }
5. 编译并运行。尝试将图片拉出屏幕。试验各种 Inertia 参数,看它们如何更改图片行为。
在此改进了一个基于鼠标的简单图片处理应用程序,将它升级成了类似于 Surface 的成熟的图片操作应用程序。