wpf - 为什么我只在滚动查看器内的元素的 Y 轴上接收触摸操作增量平移值?
问题描述
我正在尝试在 WPF 中复制默认的 Windows 10 行为以进行触摸和拖动与触摸按住并拖动操作。为什么这还不是 2021 年框架的一部分,只有 MS 霸主可以告诉我们,但是在搜索了互联网高低,尝试了各种实现,尝试在我的 WPF 应用程序中硬塞 UWP 框架等之后,我决定尝试自己实施。
为了更好地说明我所追求的并确保我们在同一页面上,我附上了以下剪辑来演示:
我已经到了看起来我的方法可以工作的地步,但是对于放置在滚动查看器中且平移模式设置为VerticalOnly的元素的 ManipulationDelta 事件似乎只提供 的值Translation.Y
,Translation.X
始终为 0。显然,我的手指不会在屏幕上完全垂直移动,所以我想接收两个轴的值。
为了实现这一点,我创建了几个自定义控件,其中第一个公开了ManipulationDelta
我可以绑定的属性,以便我可以将值提供给其他控件。它还允许我在ManipulationDeltaCallback
方法中更新控件的 RenderTransform,以便控件在屏幕上的位置发生变化:
public class Manipulatable : UserControl
{
public static readonly DependencyProperty ManipulationDeltaProperty =
DependencyProperty.Register("ManipulationDelta", typeof(ManipulationDelta), typeof(Manipulatable), new PropertyMetadata(null, new PropertyChangedCallback(ManipulationDeltaCallback)));
private TransformGroup _transformGroup;
private TranslateTransform _translation;
//private ScaleTransform scale;
//private RotateTransform rotation;
public Manipulatable()
{
_transformGroup = new TransformGroup();
_translation = new TranslateTransform(0, 0);
//scale = new ScaleTransform(1, 1);
//rotation = new RotateTransform(0);
_transformGroup.Children.Add(_translation);
//transformGroup.Children.Add(scale);
//transformGroup.Children.Add(rotation);
RenderTransform = _transformGroup;
}
public new ManipulationDelta ManipulationDelta
{
get => (ManipulationDelta)GetValue(ManipulationDeltaProperty);
set => SetValue(ManipulationDeltaProperty, value);
}
private static void ManipulationDeltaCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var manipulationDelta = e.NewValue as ManipulationDelta;
var manipulatable = d as Manipulatable;
manipulatable._translation.X += manipulationDelta.Translation.X;
manipulatable._translation.Y += manipulationDelta.Translation.Y;
}
}
第二个类再次公开了所有相关触摸操作事件的属性,因此我可以绑定到它们,以及 WPF 缺少的触摸和保持功能,因为上帝知道是什么原因:
public class TouchAndHold : Manipulatable
{
public static readonly DependencyProperty TouchedAndHeldProperty =
DependencyProperty.Register("TouchedAndHeld", typeof(RelayCommand), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty TouchedAndHeldParamProperty =
DependencyProperty.Register("TouchedAndHeldParam", typeof(object), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationStartingProperty =
DependencyProperty.Register("ManipulationStarting", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationStartedProperty =
DependencyProperty.Register("ManipulationStarted", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationDeltaChangedProperty =
DependencyProperty.Register("ManipulationDeltaChanged", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationCompletedProperty =
DependencyProperty.Register("ManipulationCompleted", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
private double length;
private bool _overrideTouch;
private bool _held;
private DispatcherTimer _touchHoldTimer;
public TouchAndHold()
{
IsManipulationEnabled = true;
_touchHoldTimer = new DispatcherTimer();
_touchHoldTimer.Tick += _touchHoldTimer_Tick;
_touchHoldTimer.Interval = new TimeSpan(5000000);
}
public RelayCommand TouchedAndHeld
{
get { return (RelayCommand)GetValue(TouchedAndHeldProperty); }
set { SetValue(TouchedAndHeldProperty, value); }
}
public object TouchedAndHeldParam
{
get { return GetValue(TouchedAndHeldParamProperty); }
set { SetValue(TouchedAndHeldParamProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationStarting
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationStartingProperty); }
set { SetValue(ManipulationStartingProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationStarted
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationStartedProperty); }
set { SetValue(ManipulationStartedProperty, value); }
}
public RelayCommand<TouchAndHoldEventArgs> ManipulationDeltaChanged
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationDeltaChangedProperty); }
set { SetValue(ManipulationDeltaChangedProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationCompleted
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationCompletedProperty); }
set { SetValue(ManipulationCompletedProperty, value); }
}
protected override void OnPreviewTouchDown(TouchEventArgs e)
{
length = 0;
_held = false;
_overrideTouch = true;
_touchHoldTimer.Start();
}
protected override void OnPreviewTouchUp(TouchEventArgs e)
{
e.Handled = !_overrideTouch || _held;
_held = false;
_overrideTouch = false;
_touchHoldTimer.Stop();
}
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationStarting?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationStarted?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
if (_held)
{
e.Handled = true;
ManipulationDeltaChanged?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
return;
}
length += e.DeltaManipulation.Translation.Length;
if (length >= 10)
{
_overrideTouch = false;
_touchHoldTimer.Stop();
return;
}
e.Handled = !_overrideTouch || _held;
}
protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationCompleted?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
private void _touchHoldTimer_Tick(object sender, EventArgs e)
{
_held = true;
_overrideTouch = false;
_touchHoldTimer.Stop();
TouchedAndHeld?.Execute(TouchedAndHeldParam);
}
public class TouchAndHoldEventArgs
{
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationStartingEventArgs eventArgs)
{
IsTouchOverriden = IsTouchOverriden;
IsHeld = isHeld;
ManipulationStartingEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationStartedEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationStartedEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationDeltaEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationDeltaEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationCompletedEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationCompletedEventArgs = eventArgs;
}
public bool IsTouchOverriden { get; }
public bool IsHeld { get; }
public ManipulationStartingEventArgs ManipulationStartingEventArgs { get; }
public ManipulationStartedEventArgs ManipulationStartedEventArgs { get; }
public ManipulationDeltaEventArgs ManipulationDeltaEventArgs { get; }
public ManipulationCompletedEventArgs ManipulationCompletedEventArgs { get; }
}
}
触摸和保持功能通过一个布尔值来工作,当设置为 true 时,确保所有操作事件都被设置为已处理 ( e.Handled = true
)。这确保了所有触摸事件都被忽略,例如,滚动查看器不会开始滚动。
当OnPreviewTouchDown
被调用时,我将布尔值设置为 true,这样事件就不会传播。我还为触摸和按住部分启动了一个计时器。同时,我监控手指在屏幕上移动的距离。如果它移动得足够远,我假设用户想要滚动,所以我停止做任何事情,让 WPF 处理它想要的一切。如果在计时器触发之前手指没有移动超过设定的限制,我认为这实际上是一个触摸和按住事件。此时我执行以下操作:
- 执行
TouchedAndHeld
命令让感兴趣的人知道发生了这个事件,并传递相关信息; - 开始监视操作事件并
ManipulationDeltaChanged
再次执行命令,以让绑定到该命令的任何人知道操作事件已发生并采取适当的措施。值得注意的是,我还设置e.Handled
为 true 以便在我在控件周围移动时滚动查看器不会开始滚动。
最后,我们准备好实际使用这个东西了。因此,我有一个带有触摸和按住控件的滚动查看器:
<ScrollViewer Style="{StaticResource MyScrollViewer}">
<ItemsControl ItemsSource="{Binding SomeButtons}" x:Name="SomeButtonsRoot">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Style="{StaticResource SomeButtonsWrapPanel}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:SomeButton Icon="{Binding Icon}" Text="{Binding Name}"
SelectCmd="{Binding ButtonSelectedCmd}" SelectCmdParam="{Binding SomeParam}"
TouchedAndHeld="{Binding DataContext.SomeButtonDragStartedCmd, ElementName=SomeButtonsRoot}"
ManipulationDeltaChanged="{Binding DataContext.SomeButtonDraggingCmd, ElementName=SomeButtonsRoot}">
<controls:SomeButton.TouchedAndHeldParam>
<MultiBinding Converter="{StaticResource List}">
<Binding />
<Binding RelativeSource="{RelativeSource Self}"/>
<Binding ElementName="CanvasRoot"/>
</MultiBinding>
</controls:SomeButton.TouchedAndHeldParam>
</controls:SomeButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
滚动查看器放置在Canvas
. 在同一内部,Canvas
我有另一个控件:
<controls:SomeButton Icon="{Binding Icon}" Text="{Binding Name}" Panel.ZIndex="100"
Canvas.Left="{Binding InitialPosition.X}" Canvas.Top="{Binding InitialPosition.Y}"
ManipulationDelta="{Binding Translation}"/>
这是实际将被拖动的控件。滚动查看器中的控件启动触摸并按住拖动,然后我将所有操作数据从启动控件传递到此控件。我没有拖动启动该过程的控件的原因是,根据堆栈溢出的this和其他答案,滚动查看器将始终剪辑到边界。因此,我没有弄乱边距、填充和 Z 索引,而是在画布中放置了一个代理控件,我可以将它移动到任何我想要的地方,并且我知道它是可见的。
到目前为止的代码很有希望。当我在滚动查看器中触摸并按住控件时,另一个控件会在其上方弹出,当我移动手指时,该控件完美地反映了我的手势,但仅在 Y 轴上。这是为什么?
解决方案
我决定看看 ScrollViewer 实际上在做什么,所以我搜索了源代码。幸运的是,我在这里找到了代码。
从第 1645 行开始,在OnManipulationStarting
覆盖内,代码检查为 ScrollViewer 设置的平移模式,并根据该值更改ManipulationModes
. ManipulationStartingEventArgs
对于PanningMode.VerticalOnly
,模式设置为ManipulationModes.TranslateY
。啊哈!这就是为什么我只得到 Y 值的原因。
为了修复这个“特性”,我只是扩展了ScrollViewer
类并覆盖了它OnManipulationStarting
,并确保操作模式保持不变:
public class TouchScrollViewer : ScrollViewer
{
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
var initialMode = e.Mode; // Keep note of the original manipulation mode.
base.OnManipulationStarting(e); // Let the ScrollViewer do it's thing.
e.Mode = initialMode; // Ensure the original manipulation mode is used.
}
}
值得庆幸的是,OnManipulationStarting
它实际上是UIElement
类的一部分,ScrollViewer
最终扩展并标记为protected
,因此我们可以在我们认为合适的情况下在扩展类中覆盖它。
我切换到使用TouchScrollViewer
in XAML 而不是原来的ScrollViewer
,现在一切都按预期工作。
我暂时不会将此标记为最终答案,以防万一有人提供更好的解决方案。
如果链接断开或页面上的代码更改,以下是相关位:
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
...
PanningMode panningMode = PanningMode;
if (panningMode != PanningMode.None)
{
...
if (ShouldManipulateScroll(e, viewport))
{
// Set Manipulation mode and container
if (panningMode == PanningMode.HorizontalOnly)
{
e.Mode = ManipulationModes.TranslateX;
}
else if (panningMode == PanningMode.VerticalOnly)
{
e.Mode = ManipulationModes.TranslateY;
}
else
{
e.Mode = ManipulationModes.Translate;
}
...
}
...
}
}
推荐阅读
- node.js - 从 Azure Blob 存储下载多个文件和文件夹 - node.js
- sql-server - 从字符串中删除前导字符
- linux - 如何在 bash 脚本中使用 find 命令检查目录大小
- visual-studio-code - 更改 ctrl+ko(新项目)目录
- java - 如何在 Launch4j 创建的 .exe 文件中打包 JavaFX?
- javascript - 修复 Redux 有效负载正在控制台中登录减速器,但在模式中说无法读取 null 的属性?
- excel - 用户表单应该只返回过滤的行
- angular - Angular 7 调用服务,承诺在调用另一个函数的组件内调用函数
- spring-webflux - 我怎样才能 bodyToMono 两次?
- python - 遍历 json 对象,然后将结果添加到数组中