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

背景

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

Fig. 1
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 主页的设计就与此相似。效果如下:

Microsoft Store Quotation

可加入快捷键控制下的横向滚动。修改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时执行水平滚动。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

brandonw3612

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值