如何在 WPF 中使用捏合来启用滚动和缩放?

How to enable both scrolling and zooming using pinch in WPF?

我正在努力使触摸事件和操作在 WPF 项目中正常工作。我有一个包含图片的 ScrollViewer,我想使用滑动手势水平和垂直滚动。另外,我想在捏合手势的中心放大 in/out。下面的代码实现了我的愿望,但存在以下问题:

我启用了 IsManipulationEnabled 并实现了缩放 in/out 功能的代码。但是,我无法将它与滚动功能结合起来(仅通过在 ScrollViewer 中设置 PanningMode)。因此,我创建了一个继承自 Image 控件的自定义控件,并覆盖了 OnTouchDownOnTouchUp 事件处理程序。基本上,我在这些被覆盖的处理程序中所做的是计算屏幕上的触摸次数和 enabling/disabling 操作。我还尝试为 ScrollViewer 设置 PanningMode,但没有成功。

下面是XAML:

<Grid>
        <ScrollViewer
            x:Name="ScrollViewerParent"
            HorizontalScrollBarVisibility="Auto"
            VerticalScrollBarVisibility="Auto"
            PanningMode="Both">
            <local:CustomImage 
                x:Name="MainImage"
                Source="{Binding Source={x:Static local:Constants.ImagePath}}"
                IsManipulationEnabled="True"
                ManipulationStarting="MainImage_ManipulationStarting"
                ManipulationDelta="MainImage_ManipulationDelta">
            </local:CustomImage>
        </ScrollViewer>
    </Grid>

这是代码隐藏:

public partial class MainWindow : Window
{
        private void MainImage_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
        {
            e.ManipulationContainer = ScrollViewerParent;
            e.Handled = true;
        }

        private void MainImage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            var matrix = MainImage.LayoutTransform.Value;

            Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);

            if (centerOfPinch == null)
            {
                return;
            }

            var deltaManipulation = e.DeltaManipulation;
            matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
            MainImage.LayoutTransform = new MatrixTransform(matrix);

            Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);

            double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
            double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;

            double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
            double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;

            double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
            double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;

            ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
            ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);

            e.Handled = true;
        }
}

自定义控件XAML:

<Style TargetType="{x:Type local:CustomImage}" />

这里是我覆盖 OnTouchDown 和 OnTouchUp 事件处理程序的地方:

 public class CustomImage : Image
    {

        private volatile int nrOfTouchPoints;
        private volatile bool isManipulationReset;
        private object mutex = new object();

        static CustomImage()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomImage), new FrameworkPropertyMetadata(typeof(CustomImage)));
        }

        protected override void OnTouchDown(TouchEventArgs e)
        {
            lock (mutex)
            {
                nrOfTouchPoints++;
                if (nrOfTouchPoints >= 2)
                {
                    IsManipulationEnabled = true;
                    isManipulationReset = false;
                }
            }
            base.OnTouchDown(e);
        }

        protected override void OnTouchUp(TouchEventArgs e)
        {
            lock (mutex)
            {
                if (!isManipulationReset)
                {
                    IsManipulationEnabled = false;
                    isManipulationReset = true;
                    nrOfTouchPoints = 0;
                }
            }
            base.OnTouchUp(e);
        }
    }

我对这段代码的期望如下:

幸运的是,我设法找到了完美的解决方案。因此,如果有人正在处理类似的问题并需要一些帮助,我将 post 给出答案。

我做了什么:

  1. 删除了自定义控件,因为它不是必需的;
  2. 创建一个计算触摸点数量的字段;
  3. 实现了TouchDown事件处理程序,将触摸点数增加1(每次设备上有触摸手势时调用此方法);
  4. 实现了TouchUp事件处理程序,将触摸点数减少1(每次设备上有向上触摸手势时调用此方法);
  5. Image_ManipulationDelta 事件处理程序中,我检查了触摸点的数量:
    • 如果触摸点数<2,则将平移值加到当前滚动条的偏移量上,从而实现滚动;
    • 否则,计算捏合中心并应用缩放手势。

这里是完整的 XAML:

 <Grid
        x:Name="GridParent">
        <ScrollViewer
            x:Name="ScrollViewerParent"
            HorizontalScrollBarVisibility="Auto"
            VerticalScrollBarVisibility="Auto"
            PanningMode="Both">
            <Image
                x:Name="MainImage"
                Source="{Binding Source={x:Static local:Constants.ImagePath}}"
                IsManipulationEnabled="True"
                TouchDown="MainImage_TouchDown"
                TouchUp="MainImage_TouchUp"
                ManipulationDelta="Image_ManipulationDelta"
                ManipulationStarting="Image_ManipulationStarting"/>
        </ScrollViewer>
    </Grid>

这是上面讨论的全部代码:

    public partial class MainWindow : Window
    {

        private volatile int nrOfTouchPoints;
        private object mutex = new object();

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        private void Image_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
        {
            e.ManipulationContainer = ScrollViewerParent;
            e.Handled = true;
        }

        private void Image_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            int nrOfPoints = 0;

            lock (mutex)
            {
                nrOfPoints = nrOfTouchPoints;
            }

            if (nrOfPoints >= 2)
            {
                DataLogger.LogActionDescription($"Executed {nameof(Image_ManipulationDelta)}");

                var matrix = MainImage.LayoutTransform.Value;

                Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);

                if (centerOfPinch == null)
                {
                    return;
                }

                var deltaManipulation = e.DeltaManipulation;
                matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
                MainImage.LayoutTransform = new MatrixTransform(matrix);

                Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);

                double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
                double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;

                double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
                double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;

                double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
                double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;

                ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
                ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);

                e.Handled = true;
            }
            else
            {
                ScrollViewerParent.ScrollToHorizontalOffset(ScrollViewerParent.HorizontalOffset - e.DeltaManipulation.Translation.X);
                ScrollViewerParent.ScrollToVerticalOffset(ScrollViewerParent.VerticalOffset - e.DeltaManipulation.Translation.Y);
            }
        }

        private void MainImage_TouchDown(object sender, TouchEventArgs e)
        {
            lock (mutex)
            {
                nrOfTouchPoints++;
            }
        }

        private void MainImage_TouchUp(object sender, TouchEventArgs e)
        {
            lock (mutex)
            {
                nrOfTouchPoints--;
            }
        }
    }
}