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

使用 Windows Phone App 時,經常會遇到的問題

「列表這麼長,Item 這麼多,為什麼我不能直接拉到指定的 Item?」

本文章旨為解決此問題,讓各位開發者都能輕鬆替自己的 ListBox 加值,成為 「ListBox、改」

本文從替內建控制項加值的角度出發,希望讀者能夠從中了解如何自行撰寫一個 CustomControl (自訂控制項)

使用 Windows Phone App 時,經常會遇到的問題

「列表這麼長,Item 這麼多,為什麼我不能直接拉到指定的 Item?」

本文章旨為解決此問題,讓各位開發者都能輕鬆替自己的 ListBox 加值,成為 「ListBox、改」

本文從替內建控制項加值的角度出發,希望讀者能夠從中了解如何自行撰寫一個 CustomControl (自訂控制項)

 

 

預計達成的目標

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

 

 

實作步驟

  1. 將 ListBoxWithScrollBar 繼承於 ListBox
  2. 於 ListBoxWithScrollBar 的外觀配置中,加入 Slider,並預計將它用來作為 ScrollBar
  3. 調整上述 Slider 的外觀配置,讓它長得像 ScrollBar 該有的樣子
  4. 讓 ScrollBar 能做出該有的動作

 

 

開始動手吧

 

 

於專案中新增一個類別 (Class),名為 ListBoxWithScrollBar

為了能讓自己撰寫用來強化 ListBox 的新控制項「ListBoxWithScrollBar」能夠相容原有的 ListBox,故直接繼承它

 


public class ListBoxWithScrollBar : ListBox

 

 

為了相容於 ListBox,我決定參考 (偷) ListBox 的 Template

隨便拉一個 ListBox,對它按右鍵 → 編輯範本 → 編輯副本,跳出建立 Style 資源視窗後,名稱不重要,定義於請選此文件

在該 XAML 檔中即可發現如下面的 Style


        <Style x:Key="ListBoxStyle1" TargetType="ListBox">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="Padding" Value="0"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListBox">
                        <ScrollViewer x:Name="ScrollViewer" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" Padding="{TemplateBinding Padding}">
                            <ItemsPresenter/>
                        </ScrollViewer>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

 

很神奇吧,這就是原生控制項 ListBox 的 Template,換句話說,這就是 ListBox 的外觀呈現

ListBox 可以滾動的原因在於裡面有一個名為 "ScrollViewer" 的 ScrollViewer 控制項

ListBox 可以產生一個個 Item 的原因在於裡面有 ItemsPresenter 控制項

 

這個 Template 需要做一些修改,才可套用在 ListBoxWithScrollBar 上

首先將整個 Template 複製下來,貼到 App.xaml 中,並注意需置於 Application.Resources 標籤內,並小心不要動到原有的項目

之所以要貼到 App.xaml 中,是因為 CustomControl 的外觀都需要置於一個全域的 Application.Resources 中

 

接下來要將這個 Template 的 TargetType 改為 ListBoxWithScrollBar


<Application
    x:Class="ListBoxWithScrollBar.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:CustomControl="clr-namespace:ListBoxWithScrollBar.Controls">

    <Application.Resources>
        <local:LocalizedStrings xmlns:local="clr-namespace:ListBoxWithScrollBar" x:Key="LocalizedStrings"/>

        <Style TargetType="CustomControl:ListBoxWithScrollBar">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="Padding" Value="0"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListBox">
                        <ScrollViewer x:Name="ScrollViewer" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" Padding="{TemplateBinding Padding}">
                            <ItemsPresenter/>
                        </ScrollViewer>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>

    <Application.ApplicationLifetimeObjects>
        <shell:PhoneApplicationService
            Launching="Application_Launching" Closing="Application_Closing"
            Activated="Application_Activated" Deactivated="Application_Deactivated"/>
    </Application.ApplicationLifetimeObjects>
</Application>

 

切回到 ListBoxWithScrollBar.cs,在建構子中加入 DefaultStyleKey = typeof(ListBoxWithScrollBar); ,如此便能使自訂控制項套用剛剛寫好的 Template


        public ListBoxWithScrollBar()
        {
            DefaultStyleKey = typeof(ListBoxWithScrollBar);
        }

 

 

在 ListBoxWithScrollBar 中加入 Slider,並且將這個 Slider 擺成直的,放在最右邊

就像一般常見的 ScrollBar 那樣


        <Style TargetType="CustomControl:ListBoxWithScrollBar">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="Padding" Value="0"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListBox">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"></ColumnDefinition>
                                <ColumnDefinition Width="auto"></ColumnDefinition>
                            </Grid.ColumnDefinitions>
                            <ScrollViewer Grid.Column="0" x:Name="ScrollViewer" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" Padding="{TemplateBinding Padding}">
                                <ItemsPresenter/>
                            </ScrollViewer>
                            <Slider Grid.Column="1" x:Name="ItemNavigateSlider" Orientation="Vertical" HorizontalAlignment="Right"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

 

到目前為止,ListBoxWithScrollBar 這個控制項就只是在 ListBox 上擺了一個毫無反應的 Slider

接下來要讓 Slider 變成真正有功能的 ScrollBar

 

切回 ListBoxWithScrollBar.cs,並在 class name 的上方加入 [TemplatePartAttribute(Name = "ItemNavigateSlider", Type = typeof(Slider))]

TemplatePartAttribute 意在宣告 (規定) 這個控制項的 Template 中必須要有一個名為 ItemNavigateSlider 的控制項,且這個控制項是 Slider 類別

如此我們便可以在 ListBoxWithScrollBar.cs 中,透過 base.GetTemplateChild("ItemNavigateSlider") as Slider; 來取得這個 Slider 的實體


    [TemplatePartAttribute(Name = "ItemNavigateSlider", Type = typeof(Slider))]
    public class ListBoxWithScrollBar : ListBox
    {
        Slider itemSlider;

        public ListBoxWithScrollBar()
        {
            DefaultStyleKey = typeof(ListBoxWithScrollBar);
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            itemSlider = base.GetTemplateChild("ItemNavigateSlider") as Slider;
        }
    }

 

取得 Slider 實體後就好辦事了

因為 Slider 在拉動時會觸發事件 ValueChanged,我們只要聽此事件,並在使用者拉動時,對 ListBox  做相對應處理

站在巨人的肩膀上開發,真省事 :D

 

我希望滑動 Slider 時,可同時將 ListBox 滑動到相對應的 Item

所以 Slider 的最大值應與目前 Items 內的數量一致,並且將 Slider 的最小更動幅度設為 1.0,以符合預期的「Slider 動一格,ListBox 就動一格」


        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.ValueChanged += itemSlider_ValueChanged;
                }
            }
        }

 

當 Slider.ValueChanged 觸發時,我們要透過 ListBox 已經撰寫好的 ScrollIntoView 方法,將畫面捲動至該去的地方

 

但是,該捲動到哪呢?

ListBox 的 Item,其 Index 是越下面越大

但擺直的 Slider (Orientation="Vertical") ,其值卻是越下面越小

這…似乎有點麻煩

只好以差值 (最大值 - 目前值) 的方式來解決此問題

其實 Slider 有一個屬性叫 IsDirectionReversed,可以讓值的遞增遞減方向相反,這就留給讀者去測試囉


        private void itemSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            Slider targetSlider = sender as Slider;
            if (targetSlider != null)
            {
                Int32 scrollItemIndex = (Int32)(targetSlider.Maximum - targetSlider.Value);
                if (this.Items.Count >= scrollItemIndex)
                {
                    Object targetItem = this.Items.ElementAt(scrollItemIndex);
                    this.ScrollIntoView(targetItem);
                }
            }
        }

 

到目前為止,已經實現了拉動 ListBoxWithScrollBar 中的 ScrollBar,快速切換到想看的地方

但是,仍有一些問題需要解決

1. 這個 ScrollBar 長得很突兀,它明明就是直的 Slider

2. 滑動 ListBox,旁邊的 ScrollBar 沒有跟著移動

 

 

本文的範例程式

 

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