欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

【UWP】多个ScrollViewer嵌套时鼠标滚轮事件的处理

程序员文章站 2022-03-02 14:45:55
背景项目需要实现下图的布局: 页面根部为垂直滚动的ScrollViewer,其中包含纵向排列的StackPanel,StackPanel的子元素中包含横向滚动的ScrollViewer。UWP提供的ScrollViewer控件对于触摸屏、触控板的操作逻辑适应良好,可随用户手势进行滚动,但对于鼠标滚轮就有些麻烦。默认情况下,当鼠标滚轮在HorizontalScrollViewer内滚动时,仅HorizontalScrollViewer内的布局面板发生滚动,而外层的VerticalScrollViewer...

背景

项目需要实现下图的布局: 页面根部为垂直滚动的ScrollViewer,其中包含纵向排列的StackPanel,StackPanel的子元素中包含横向滚动的ScrollViewer。

【UWP】多个ScrollViewer嵌套时鼠标滚轮事件的处理
UWP提供的ScrollViewer控件对于触摸屏、触控板的操作逻辑适应良好,可随用户手势进行滚动,但对于鼠标滚轮就有些麻烦。默认情况下,当鼠标滚轮在HorizontalScrollViewer内滚动时,仅HorizontalScrollViewer内的布局面板发生滚动,而外层的VerticalScrollViewer是不发生变化的。但为了提供一致的体验,在用户使用鼠标滚轮滚动浏览页面时,应消除此逻辑引起的“体验割裂”。

解决方案

先上XAML:

<ScrollViewer x:Name="VerticalScrollViewer">
            <StackPanel Spacing="20" Name="VerticalPanel">
                <Rectangle Height="300" Width="1000" Fill="Black" />
                <Rectangle Height="300" Width="1000" Fill="Black" />
                <ScrollViewer VerticalScrollMode="Disabled" HorizontalScrollMode="Auto" HorizontalScrollBarVisibility="Visible">
                    <StackPanel Orientation="Horizontal" Spacing="20" PointerWheelChanged="StackPanel_PointerWheelChanged">
                        <Rectangle Height="300" Width="1000" Fill="Red" />
                        <Rectangle Height="300" Width="1000" Fill="Red" />
                        <Rectangle Height="300" Width="1000" Fill="Red" />
                        <Rectangle Height="300" Width="1000" Fill="Red" />
                    </StackPanel>
                </ScrollViewer>
                <Rectangle Height="300" Width="1000" Fill="Black" />
                <Rectangle Height="300" Width="1000" Fill="Black" />
            </StackPanel>
        </ScrollViewer>

注意到ScrollViewer本身是不会响应PointerWheelChanged这个事件的,应在横向滚动视图的子控件(横向StackPanel)声明。以下为C#代码:

private void StackPanel_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
    int delta = e.GetCurrentPoint(sender as UIElement).Properties.MouseWheelDelta;
    VerticalScrollViewer.ChangeView(VerticalScrollViewer.HorizontalOffset, VerticalScrollViewer.VerticalOffset - delta, 1);
    e.Handled = true;
}

参考微软的UWP文档,利用传入的PointerRoutedEventArgs属性获取Properties,进而得到鼠标滚动滑动的增量,接着调用VerticalScrollViewer的ChangeView方法,横向Offset不变,纵向调整为原Offset减去鼠标滚轮滑动增量。

*注:若此处使用VerticalScrollViewer.VerticalOffset + delta会与用户原有鼠标操作习惯矛盾。关于MouseWheelDelta的正负,文档解释如下:

A positive value indicates that the wheel was rotated forward (away from the user) or tilted to the right; a negative value indicates that the wheel was rotated backward (toward the user) or tilted to the left.
正值表示鼠标滚轮向前(远离用户方向)或向右滚动;负值表示鼠标滚轮向后(靠近用户方向)或向左滚动。

此处向前滚动,按鼠标操作习惯即向上滚动,但依照ScrollViewer的坐标计算逻辑,向上滚动应减小Offset的值,故使用相减。

最后将传入PointerRoutedEventArgs的Handled属性设置为true,避免横向

ItemsControl的处理

考虑到用户使用鼠标操作时仍有横向滚动的需求,故在ItemsControl的ScrollViewer上层左右两侧放置按钮用于滚动。定义ControlTemplate如下:

<ControlTemplate x:Key="HorizontalScrollingDisabledGridViewTemplate"
			     TargetType="GridView">
    <Grid Background="{TemplateBinding Background}"
    	  BorderThickness="{TemplateBinding BorderThickness}"
    	  BorderBrush="{TemplateBinding BorderBrush}"
    	  CornerRadius="{TemplateBinding CornerRadius}">
        <Grid.Resources>
            <ResourceDictionary>
                <SolidColorBrush x:Key="ButtonBackground" Color="{ThemeResource SystemAltMediumColor}" />
                <SolidColorBrush x:Key="ButtonBorderBrush" Color="Transparent" />
                <SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="{ThemeResource SystemAltMediumHighColor}" />
                <SolidColorBrush x:Key="ButtonBorderBrushPointerOver" Color="Transparent" />
                <SolidColorBrush x:Key="ButtonBackgroundPressed" Color="{ThemeResource SystemAltHighColor}" />
            </ResourceDictionary>
        </Grid.Resources>
        <ScrollViewer x:Name="ScrollViewer"
        			  AutomationProperties.AccessibilityView="Raw"
        			  BringIntoViewOnFocusChange="{TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}"
        			  HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
        			  HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
        			  IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
        			  IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}"
        			  IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
        			  IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
        			  IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}"
        			  TabNavigation="{TemplateBinding TabNavigation}"
        			  VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
        			  VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
        			  ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}">
            <ItemsPresenter Loaded="HorizontalScrollingDisabledGridViewItemsPresenter_Loaded"
            				Footer="{TemplateBinding Footer}"
            				FooterTransitions="{TemplateBinding FooterTransitions}"
            				FooterTemplate="{TemplateBinding FooterTemplate}"
            				Header="{TemplateBinding Header}"
            				HeaderTransitions="{TemplateBinding HeaderTransitions}"
            				HeaderTemplate="{TemplateBinding HeaderTemplate}"
            				Padding="{TemplateBinding Padding}" />
        </ScrollViewer>
        <Button Visibility="Collapsed" Click="HorizontalScrollingDisabledGridViewStepButton_Click"
        		Width="50" Height="50" CornerRadius="25" Margin="10"
        		HorizontalAlignment="Left" Tag="Left">
            <FontIcon Glyph="&#xE76B;" />
        </Button>
        <Button Click="HorizontalScrollingDisabledGridViewStepButton_Click"
        		Width="50" Height="50" CornerRadius="25" Margin="10"
        		HorizontalAlignment="Right" Tag="Right">
            <FontIcon Glyph="&#xE76C;" />
        </Button>
    </Grid>
</ControlTemplate>

该ControlTemplate中引用了两个方法 HorizontalScrollingDisabledGridViewItemsPresenter_Loaded 和 HorizontalScrollingDisabledGridViewStepButton_Click 。与前文同理,代码如下:

private void HorizontalScrollingDisabledGridViewItemsPresenter_Loaded(object sender, RoutedEventArgs e)
{
    var ParentScrollViewer = (sender as ItemsPresenter).Parent as ScrollViewer;
    // 利用 VisualTreeHelper 获取 ParentScrollViewer 的 Content Border ,监听 Border 的 PointerWheelChanged 事件,原理同上
    (VisualTreeHelper.GetChild(ParentScrollViewer, 0) as Border).PointerWheelChanged += (sender, e) =>
    {
        int delta = e.GetCurrentPoint(sender as UIElement).Properties.MouseWheelDelta;
        RootScrollViewer.ChangeView(RootScrollViewer.HorizontalOffset, RootScrollViewer.VerticalOffset - delta, 1);
        e.Handled = true;
    };
    // 监听 ParenScrollViewer 的 ViewChanged 事件,控制左右两侧按钮的显示
    ParentScrollViewer.ViewChanged += (sender, e) =>
    {
        var ParentGrid = VisualTreeHelper.GetParent(ParentScrollViewer) as Grid;
        (ParentGrid.Children[1] as Button).Visibility = (sender as ScrollViewer).HorizontalOffset > 0 ? Visibility.Visible : Visibility.Collapsed;
        (ParentGrid.Children[2] as Button).Visibility = (sender as ScrollViewer).HorizontalOffset + (sender as ScrollViewer).ActualWidth < ((sender as ScrollViewer).Content as ItemsPresenter).ActualWidth ? Visibility.Visible : Visibility.Collapsed;
    };
}

private void HorizontalScrollingDisabledGridViewStepButton_Click(object sender, RoutedEventArgs e)
{
	// 利用 Button 的 Tag 标定 ScrollViewer 滚动的方向
    double factor = (sender as Button).Tag.ToString() == "Left" ? -1 :
        (sender as Button).Tag.ToString() == "Right" ? 1 : 0;
    var HorizontalScrollViewer = ((sender as Button).Parent as Grid).Children[0] as ScrollViewer;
    // ScrollViewer 每次滚动半屏距离
    HorizontalScrollViewer.ChangeView(HorizontalScrollViewer.HorizontalOffset + factor * HorizontalScrollViewer.ActualWidth * 0.5, HorizontalScrollViewer.VerticalOffset, 1);
}

其中,前一个 Loaded 事件的响应也可以在 ScrollViewer 加载时完成。(一开始觉得应该监听 ItemsPresenter 的 PointerWheelChanged 事件,但测试发现 GridView 的其他区域(Header、Padding等)鼠标滚轮事件未被截获,故修改为上文方法。)

相似样例

Microsoft Store 主页的设计就与此相似。效果如下:

【UWP】多个ScrollViewer嵌套时鼠标滚轮事件的处理

可加入快捷键控制下的横向滚动。修改Border的PointerWheelChanged响应代码如下:

int delta = e.GetCurrentPoint(sender as UIElement).Properties.MouseWheelDelta;
if (e.KeyModifiers == Windows.System.VirtualKeyModifiers.Control || e.KeyModifiers == Windows.System.VirtualKeyModifiers.Shift)
    ParentScrollViewer.ChangeView(ParentScrollViewer.HorizontalOffset - delta, ParentScrollViewer.VerticalOffset, 1);
else RootScrollViewer.ChangeView(RootScrollViewer.HorizontalOffset, RootScrollViewer.VerticalOffset - delta, 1);
e.Handled = true;

至此,鼠标滚轮操作时执行垂直滚动,按下Control或Shift时执行水平滚动。

本文地址:https://blog.csdn.net/brandonw3612/article/details/109228923