今天,要用WPF实现一个可以通过Windows触屏左右滑动的ListBox控件,并且,同时也可以通过点击两个按钮,进行左右滑动。
实现这个控件,有几个难点:
两种方式,都需要有一个共同的值或方式来记录滑动的距离和方向。否则通过一种方式滑动以后,再用另外一种方式,就会出现错误的距离滑动。
滑动的距离不容易获取,因为ListBox没有类似于OffSet的属性。
当ListBox的内容的元素比较多的时候,也就是可以滑动的时候,不容易得知那些元素是露在外面的。
Xaml的相关代码:
<ListBox x:Name="navItemsListBox" ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.CanContentScroll="False" ScrollViewer.IsDeferredScrollingEnabled="True" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingPanel.ScrollUnit="Item" Background="Transparent" BorderThickness="0" Width="840"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"> </StackPanel> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <Label Content="{Binding CategoryTitle}" Width="70" Height="35"/> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Width="30" Height="30" Click="CategoryToLeft"/> <Button Width="30" Height="30" Click="CategoryToRight"/>
有种方法,可以实现第二种方式滚动:
navItemsListBox.ScrollIntoView(navItemsListBox.Items[++CurrentRightIndex]);
这种方法,可以让ListBox滚动到其包含的某个元素。但是因为不能记录滚动的具体位置,所以对第一种划屏的方式放无能为力,所以这种方法不能采用。
有种方法,可以完美实现。
首先通过VisualTree的方法获得内嵌在ListBox中的ScrollViewer:
Decorator border = VisualTreeHelper.GetChild(navItemsListBox, 0) as Decorator; if (border != null) { scrollViewer = border.Child as ScrollViewer; if (scrollViewer != null) { scrollViewer.ScrollToHorizontalOffset(theOffset); } }
因为ScrollViewer是可以记录滚动的偏差的,比如HorizontalOffset属性就是记录水平的滚动偏差的。获取了内嵌在ListBox中的ScrollViewer,也就间接的获取了ListBox的滚动偏差。
然后,获取 navItemsListBox
中的 ListBoxItem
,用来计算每个元素的宽度和ListBox左右能滑动的最大宽度:
ListBoxItem theItem = (ListBoxItem)(navItemsListBox.ItemContainerGenerator.ContainerFromIndex(0)); var itemWidth = theItem.ActualWidth; var itemsTotalWidth = (navItemsListBox.Items.Count - MAXSHOWNINDEX) * itemWidth;
其中,MAXSHOWNINDEX是ListBox能同时展现在外面的最多元素的个数。
那么这个方法的原理就是:因为无论是通过触屏滑屏还是点击按钮滚动,都会改变ListBox中ScrollViewer的HorizontalOffset的值(当然,也会触发ListBox的ScrollViewer.ScrollChanged事件,我就是通过这个事件来了解这个方法的),那么每次在点击按钮进行滚动的时候,首先要获取当前的HorizontalOffset的值,然后再用
scrollViewer.ScrollToHorizontalOffset(theOffset);
这个方法再次改变HorizontalOffset。这样,就通过HorizontalOffset这个值来统一管理两种滚动方法的偏移量了。
当然,以上功能可以直接采用ScrollViewer实现,但是由于历史代码遗留的原因,不得不采用ListBox。
最终的code-behind代码如下:
private const int MAXSHOWNINDEX = 9; private ScrollViewer scrollViewer = new ScrollViewer(); private double theOffset = 0; private void UserControl_Loaded(object sender, RoutedEventArgs e) { Decorator border = VisualTreeHelper.GetChild(navItemsListBox, 0) as Decorator; if (border != null) { scrollViewer = border.Child as ScrollViewer; if (scrollViewer != null) { scrollViewer.ScrollToHorizontalOffset(theOffset); } } } private void CategoryToRight(object sender, RoutedEventArgs e) { ListBoxItem theItem = (ListBoxItem)(navItemsListBox.ItemContainerGenerator.ContainerFromIndex(0)); var itemWidth = theItem.ActualWidth; var itemsTotalWidth = (navItemsListBox.Items.Count - MAXSHOWNINDEX) * itemWidth; theOffset = scrollViewer.HorizontalOffset; if (theOffset + itemWidth > itemsTotalWidth) { theOffset = itemsTotalWidth; } else theOffset += itemWidth; scrollViewer.ScrollToHorizontalOffset(theOffset); } private void CategoryToLeft(object sender, RoutedEventArgs e) { ListBoxItem theItem = (ListBoxItem)(navItemsListBox.ItemContainerGenerator.ContainerFromIndex(0)); var itemWidth = theItem.ActualWidth; theOffset = scrollViewer.HorizontalOffset; if (theOffset - itemWidth < 0) { theOffset = 0; } else theOffset -= itemWidth; scrollViewer.ScrollToHorizontalOffset(theOffset); }