[Windows Phone 開發] 為 ListBox 增加功能吧 - 加入快速導覽 ScrollBar (下)

在上集內,介紹了如何站在巨人的肩膀上加值內建控制項,自行撰寫一個新控制項 ListBoxWithScrollBar

並實作了以滑動 ScrollBar 的方式,快速移動 ListBox 畫面的方法

接下來,透過本文介紹,將讓 Slider 變得更像 ScrollBar

並且當 ListBox 滑動時,能讓 Slider 滑動至相對應的位置

在上集內,介紹了如何站在巨人的肩膀上加值內建控制項,自行撰寫一個新控制項 ListBoxWithScrollBar

並實作了以滑動 ScrollBar 的方式,快速移動 ListBox 畫面的方法

接下來,透過本文介紹,將讓 Slider 變得更像 ScrollBar

並且當 ListBox 滑動時,能讓 Slider 滑動至相對應的位置

 

 

首先是外觀,與預期中的 ScrollBar 實在差太多了

所以我想要先取得 Slider 的內建 Template

同上集步驟,隨便拉一個 Slider,並在上面按右鍵 → 編輯範本 → 編輯副本,跳出建立 Style 資源視窗後,名稱取為 ScrollBarSliderStyle,定義於則選應用程式


 <Style x:Key="ScrollBarSliderStyle" TargetType="Slider">
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="Maximum" Value="10"/>
            <Setter Property="Minimum" Value="0"/>
            <Setter Property="Value" Value="0"/>
            <Setter Property="Background" Value="{StaticResource PhoneChromeBrush}"/>
            <Setter Property="Foreground" Value="{StaticResource PhoneAccentBrush}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Slider">
                        <Grid Background="Transparent">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="MouseOver"/>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" To="0.1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="HorizontalTrack"/>
                                            <DoubleAnimation Duration="0" To="0.1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="VerticalTrack"/>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Fill" Storyboard.TargetName="HorizontalFill">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Fill" Storyboard.TargetName="VerticalFill">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Grid x:Name="HorizontalTemplate" Margin="{StaticResource PhoneHorizontalMargin}">
                                <Rectangle x:Name="HorizontalTrack" Fill="{TemplateBinding Background}" Height="12" IsHitTestVisible="False" Margin="0,22,0,50"/>
                                <Rectangle x:Name="HorizontalFill" Fill="{TemplateBinding Foreground}" Height="12" IsHitTestVisible="False" Margin="0,22,0,50">
                                    <Rectangle.Clip>
                                        <RectangleGeometry Rect="0, 0, 6, 12"/>
                                    </Rectangle.Clip>
                                </Rectangle>
                                <Rectangle x:Name="HorizontalCenterElement" Fill="{StaticResource PhoneForegroundBrush}" HorizontalAlignment="Left" Height="24" Margin="0,16,0,44" Width="12">
                                    <Rectangle.RenderTransform>
                                        <TranslateTransform/>
                                    </Rectangle.RenderTransform>
                                </Rectangle>
                            </Grid>
                            <Grid x:Name="VerticalTemplate" Margin="{StaticResource PhoneVerticalMargin}">
                                <Rectangle x:Name="VerticalTrack" Fill="{TemplateBinding Background}" IsHitTestVisible="False" Margin="18,0,18,0" Width="12"/>
                                <Rectangle x:Name="VerticalFill" Fill="{TemplateBinding Foreground}" IsHitTestVisible="False" Margin="18,0,18,0" Width="12">
                                    <Rectangle.Clip>
                                        <RectangleGeometry Rect="0, 0, 12, 6"/>
                                    </Rectangle.Clip>
                                </Rectangle>
                                <Rectangle x:Name="VerticalCenterElement" Fill="{StaticResource PhoneForegroundBrush}" Height="12" Margin="12,0,12,0" VerticalAlignment="Top" Width="24">
                                    <Rectangle.RenderTransform>
                                        <TranslateTransform/>
                                    </Rectangle.RenderTransform>
                                </Rectangle>
                            </Grid>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

 

由於此次我們用到的是直的 Slider,所以只需要針對 VerticalTemplate 修改

我想要讓 Slider 的條變得細一點,所以將 Rectangle 的 Width 改為 5

Slider 只能有一種顏色,所以將兩個 Rectangle 的 Fill 都設為 {TemplateBinding Background}

而調整用的 Bar 則從長方形 Rectangle 改成圓形 Ellipse


                            <Grid x:Name="VerticalTemplate" Margin="{StaticResource PhoneVerticalMargin}">
                                <Rectangle x:Name="VerticalTrack" Fill="{TemplateBinding Background}" IsHitTestVisible="False" Margin="18,0,18,0" Width="5"/>
                                <Rectangle x:Name="VerticalFill" Fill="{TemplateBinding Background}" IsHitTestVisible="False" Margin="18,0,18,0" Width="5">
                                    <Rectangle.Clip>
                                        <RectangleGeometry Rect="0, 0, 12, 6"/>
                                    </Rectangle.Clip>
                                </Rectangle>
                                <Ellipse x:Name="VerticalCenterElement" Fill="White" Height="30" Margin="12,0,12,0" VerticalAlignment="Top" Width="30">
                                    <Ellipse.RenderTransform>
                                        <TranslateTransform/>
                                    </Ellipse.RenderTransform>
                                </Ellipse>
                            </Grid>

 

此樣式完成後,回到 App.xaml,將 ListBoxWithScrollBar Template 中的 ItemNavigateSlider 套用 Style="{StaticResource ScrollBarSliderStyle}" Background="Blue"

此時 Slider 已經與我們想像中的 ScrollBar 一模一樣了


                            <Slider Grid.Column="1" x:Name="ItemNavigateSlider" Orientation="Vertical" HorizontalAlignment="Right" Style="{StaticResource ScrollBarSliderStyle}" Background="Blue"/>

 

 

下一個任務,當 ListBox 滑動時,Slider 也要滑到對應的位置

由於 ListBox 滑動時並不會觸發任何事件 (真可惡...

所以我們要自己想辦法處理

 

在上集中提到,ListBox 的組成一個 ScrollViewer 內包著 ItemPresenter

ScrollViewer 內含有兩個 ScrollBar,分別代表橫移與直移

剛好 ScrollBar 提供了 ValueChanged 事件,當 ScrollBar 被滑動時會觸發

所以我們可以想辦法找出藏在 ListBox 中的 ScrollViewer 中的 ScrollBar (好繞口)

並在 ValueChanged 事件觸發時,調整 Slider 的 Value,使得 ListBox 與 Slider 位置一致

在開發 Windows Phone 自訂控制項時,經常需要像這樣一層一層地往上挖

故建議閒暇沒事時可以多翻翻各個內建控制項的 Template,看看它們是由哪些控制項組成的

 

要找到藏在控制項中的控制項,必須靠 VisualTreeHelper

在此利用兩個函數來取得控制項中的控制項


        private static List<T> GetVisualChildCollection<T>(object parent) where T : UIElement
        {
            List<T> visualCollection = new List<T>();
            GetVisualChildCollection(parent as DependencyObject, visualCollection);
            return visualCollection;
        }

        private static void GetVisualChildCollection<T>(DependencyObject parent, List<T> visualCollection) where T : UIElement
        {
            int count = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < count; i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(parent, i);
                if (child is T)
                    visualCollection.Add(child as T);
                else if (child != null)
                    GetVisualChildCollection(child, visualCollection);
            }
        }

先聽 ListBoxWithScrollBar.OnLoaded 


        public ListBoxWithScrollBar()
        {
            DefaultStyleKey = typeof(ListBoxWithScrollBar);
            this.Loaded += ListBoxWithScrollBar_Loaded;
        }

 

於 ListBoxWithScrollBar.OnLoaded 時,利用 GetVisualChildCollection<ScrollBar>(this); 取得在 ListBoxWithScrollBar 中的所有 ScorllBar

並聽取 ScrollBar 的 ValueChanged 事件


        private void ListBoxWithScrollBar_Loaded(object sender, RoutedEventArgs e)
        {
            List<ScrollViewer> controlScrollViewerList = GetVisualChildCollection<ScrollViewer>(sender);

            if (controlScrollViewerList != null && controlScrollViewerList.Count > 0)
            {
                List<ScrollBar> controlScrollBarList = GetVisualChildCollection<ScrollBar>(controlScrollViewerList.First());

                if (controlScrollBarList != null && controlScrollBarList.Count > 0)
                {
                    controlScrollBarList.ForEach(scrollBar =>
                    {
                        scrollBar.ValueChanged += scrollBar_ValueChanged;
                    });
                }
            }
        }

        private void scrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            ScrollBar targetScrollBar = sender as ScrollBar;
            if (targetScrollBar != null && targetScrollBar.Maximum > 0 && itemSlider != null)
            {
                double ratio = e.NewValue / targetScrollBar.Maximum;
                itemSlider.Value = itemSlider.Maximum * (1 - ratio);
            }
        }

現在的情境是

Slider 滑動 -> ListBox 移動至相對應位置

ListBox 移動 -> Slider 滑動至相對應位置

 

噢,這裡產生一個迴圈,導致 ListBox 無法滑動了

我們必須避免此情況

 

試著將情況修改為

Slider.MouseEnter -> 開始聽 Slider.ValueChanged 事件 -> Slider 滑動位置 -> ListBox 移動至相對應位置 -> Slider.MouseLeave -> 停止聽 Slider.ValueChanged 事件

便能夠阻止迴圈

 

於 OnApplyTemplate 中,讓 itemSlider 聽 MouseEnter 事件與 MouseLeave 事件


        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            itemSlider = base.GetTemplateChild("ItemNavigateSlider") as Slider;
            if (itemSlider != null)
            {
                if (this.Items == null)
                {
                    itemSlider.Visibility = System.Windows.Visibility.Collapsed;
                }
                else
                {
                    itemSlider.Maximum = this.Items.Count - 1;
                    itemSlider.SmallChange = 1.0;
                    itemSlider.LargeChange = 10.0;
                    itemSlider.Value = itemSlider.Maximum;
                    itemSlider.MouseEnter += itemSlider_MouseEnter;
                    itemSlider.MouseLeave += itemSlider_MouseLeave;
                }
            }
        }

 

於 itemSlider_MouseEnter 中開始聽 itemSlider.ValueChanged 事件


        private void itemSlider_MouseEnter(object sender, RoutedEventArgs e)
        {
            itemSlider.ValueChanged += itemSlider_ValueChanged;
        }

 

於 itemSlider_MouseLeave 中停止聽 itemSlider.ValueChanged 事件


        private void itemSlider_MouseLeave(object sender, RoutedEventArgs e)
        {
            itemSlider.ValueChanged -= itemSlider_ValueChanged;
        }

 

 

回顧上一集的目標

  • 撰寫一個新的控制項 ListBoxWithScrollBar,提供使用者快速切換到想看的地方
  • 避免現有程式更動幅度過大,新控制項必須相容舊有的 ListBox

此時我們的 ListBoxWithScrollBar 已經可以完全取代 ListBox,並且讓 ScrollBar 生效

 

 

本文的範例程式

 

 

達成替內建控制項加值的成就

達成撰寫個人生涯第一個技術系列文章的成就

 

[Windows Phone 開發] 為 ListBox 增加功能吧 - 加入快速導覽 ScrollBar (上)