【UWP】多个ScrollViewer嵌套时鼠标滚轮事件的处理
背景
项目需要实现下图的布局: 页面根部为垂直滚动的ScrollViewer,其中包含纵向排列的StackPanel,StackPanel的子元素中包含横向滚动的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="" />
</Button>
<Button Click="HorizontalScrollingDisabledGridViewStepButton_Click"
Width="50" Height="50" CornerRadius="25" Margin="10"
HorizontalAlignment="Right" Tag="Right">
<FontIcon Glyph="" />
</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 主页的设计就与此相似。效果如下:
续
可加入快捷键控制下的横向滚动。修改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