项目需要实现下图的布局: 页面根部为垂直滚动的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的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时执行水平滚动。
