在 WPF 中,框架可以分為兩個部分,一個是渲染,另一個是交互。交互的入口是在 InputManager 裡面,而實際的交互實現需要通過渲染佈局和交互的路由事件才能完成。在輸入管理提供了調度事件的方法,這個方法可以被傳入路由事件,傳入的路由事件將會被調度到路由事件指定的元素上進行觸發。本文告訴大家如... ...
在 WPF 中,框架可以分為兩個部分,一個是渲染,另一個是交互。交互的入口是在 InputManager 裡面,而實際的交互實現需要通過渲染佈局和交互的路由事件才能完成。在輸入管理提供了調度事件的方法,這個方法可以被傳入路由事件,傳入的路由事件將會被調度到路由事件指定的元素上進行觸發。本文告訴大家如何模擬調度一個觸摸事件
本文的內容屬於沒有任何官方文檔的支持的內容,以下是我看 WPF 源代碼瞭解到的用法
在輸入管理裡面可以通過 System.Windows.Input.InputManager.Current 拿到當前的輸入管理,這個屬性預設和 Dispatcher.CurrentDispatcher.InputManager 是相同的對象,只有在初始化的時候 Dispatcher.CurrentDispatcher.InputManager 會是空拿不到值,而通過 System.Windows.Input.InputManager.Current 將會自動創建
此時就可以回答這個 InputManager.Current 是針對進程還是線程的問題了,請問 CurrentDispatcher 是針對進程還是線程呢
在拿到輸入管理,就可以調用 ProcessInput 方法傳入一個 InputEventArgs 了,可以傳入一個路由事件,此時路由事件將會加入觸發隊列,在調度方法的核心是通過 Stack _stagingArea
欄位做到棧的方式的調度
/// <summary>
/// Synchronously processes the specified input.
/// </summary>
/// <remarks>
/// The specified input is processed by all of the filters and
/// monitors, and is finally dispatched to the appropriate
/// element as an input event.
/// </remarks>
/// <returns>
/// Whether or not any event generated as a consequence of this
/// event was handled.
/// </returns>
public bool ProcessInput(InputEventArgs input)
{
// VerifyAccess();
if(input == null)
{
throw new ArgumentNullException("input");
}
// Push a marker indicating the portion of the staging area
// that needs to be processed.
PushMarker();
// Push the input to be processed onto the staging area.
PushInput(input, null);
// Post a work item to continue processing the staging area
// in case someone pushes a dispatcher frame in the middle
// of input processing.
RequestContinueProcessingStagingArea();
// Now drain the staging area up to the marker we pushed.
bool handled = ProcessStagingArea();
return handled;
}
上面代碼核心的邏輯是 ProcessStagingArea 方法
簡化的代碼應該和下麵差不多
while((item = PopInput()) != null)
{
// 忽略 Pre-Process 邏輯
// Raise the input event being processed.
InputEventArgs input = item.Input;
// Some input events are explicitly associated with an element. Those that are not are associated with the target of the input device for this event.
// 有些輸入的元素是和輸入事件關聯的,此時和輸入設備沒有關係
// 上面的註釋說的是先通過 input.Source 獲取和輸入事件關聯的元素,如果不能獲取到,那麼也許輸入元素是和輸入設備關聯的,嘗試從輸入設備獲取
DependencyObject eventSource = input.Source as DependencyObject;
if (eventSource == null)
{
eventSource = input.Device.Target as DependencyObject;
}
if (InputElement.IsUIElement(eventSource))
{
UIElement e = (UIElement)eventSource;
e.RaiseEvent(input, true); // Call the "trusted" flavor of RaiseEvent.
}
else if (InputElement.IsContentElement(eventSource))
{
ContentElement ce = (ContentElement)eventSource;
ce.RaiseEvent(input, true);// Call the "trusted" flavor of RaiseEvent.
}
else if (InputElement.IsUIElement3D(eventSource))
{
UIElement3D e3D = (UIElement3D)eventSource;
e3D.RaiseEvent(input, true); // Call the "trusted" flavor of RaiseEvent
}
}
上面的 PopInput 方法如下
internal StagingAreaInputItem PopInput()
{
object input = null;
if(_stagingArea.Count > 0)
{
input = _stagingArea.Pop();
}
return input as StagingAreaInputItem;
}
也就是本質上都是調用了元素的 RaiseEvent 方法,裡面沒有什麼判斷邏輯
按照上面的邏輯,咱可以嘗試自己模擬觸發觸摸事件。不過創建一個 TouchEventArgs 還是比較複雜的邏輯,需要用 WPF 模擬觸摸設備
但是簡單的測試是可以通過觸摸一下屏幕,保存觸摸事件的參數
private void OnTouchDown(object sender, TouchEventArgs e)
{
_lastEventArgs = e;
}
private TouchEventArgs _lastEventArgs;
下麵嘗試在滑鼠按下的時候觸發這個事件
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.StylusDevice != null)
{
}
else
{
System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs);
}
}
在觸摸之後點擊滑鼠,可以看到滑鼠點擊的時候同樣觸發了觸摸按下事件
那如果想要模擬觸發觸摸移動的事件呢?可以嘗試修改 RoutedEvent 屬性
_lastEventArgs.RoutedEvent = PreviewTouchDownEvent;
System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs);
_lastEventArgs.RoutedEvent = PreviewTouchMoveEvent;
System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs);
_lastEventArgs.RoutedEvent = PreviewTouchUpEvent;
System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs);
上面圖片是測試工具 ManipulationDemo 的顯示,這個工具會在事件觸發的時候修改對應事件顏色,也就是在滑鼠點擊的時候觸發了觸摸的按下和移動和抬起
用這個方法就可以從路由事件這一層調度事件
上面的代碼放在 GitHub 上,小伙伴打開代碼需要關註的是 OnMouseDown 方法的代碼
根據上面的源代碼可以知道框架裡面其實也是調用了 RaiseEvent 方法,也就是不使用交互框架的調度自己觸發是否可以?實際上也是可以的
只需要將 System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs)
替換為 ((UIElement)_lastEventArgs.Source).RaiseEvent(_lastEventArgs)
請看代碼
_lastEventArgs.RoutedEvent = PreviewTouchDownEvent;
((UIElement)_lastEventArgs.Source).RaiseEvent(_lastEventArgs);
//System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs);
_lastEventArgs.RoutedEvent = PreviewTouchMoveEvent;
((UIElement)_lastEventArgs.Source).RaiseEvent(_lastEventArgs);
//System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs);
_lastEventArgs.RoutedEvent = PreviewTouchUpEvent;
((UIElement)_lastEventArgs.Source).RaiseEvent(_lastEventArgs);
//System.Windows.Input.InputManager.Current.ProcessInput(_lastEventArgs);
此時運行測試項目也可以看到和 ProcessInput 一樣的效果
本文其實是補充 WPF 觸摸到事件 的後半部分,從 WPF 觸摸到路由事件,是如何從觸摸事件讓對應的元素觸發
本文的方法僅是模擬事件的觸發,如果想要修改觸摸的點的坐標等,需要自己實現 TouchDevice 類,請看 WPF 模擬觸摸設備