使用 Windows Phone App 時,經常會遇到的問題
「列表這麼長,Item 這麼多,為什麼我不能直接拉到指定的 Item?」
本文章旨為解決此問題,讓各位開發者都能輕鬆替自己的 ListBox 加值,成為 「ListBox、改」
本文從替內建控制項加值的角度出發,希望讀者能夠從中了解如何自行撰寫一個 CustomControl (自訂控制項)
使用 Windows Phone App 時,經常會遇到的問題
「列表這麼長,Item 這麼多,為什麼我不能直接拉到指定的 Item?」
本文章旨為解決此問題,讓各位開發者都能輕鬆替自己的 ListBox 加值,成為 「ListBox、改」
本文從替內建控制項加值的角度出發,希望讀者能夠從中了解如何自行撰寫一個 CustomControl (自訂控制項)
預計達成的目標
- 撰寫一個新的控制項 ListBoxWithScrollBar,提供使用者快速切換到想看的地方
- 避免現有程式更動幅度過大,新控制項必須相容舊有的 ListBox
實作步驟
- 將 ListBoxWithScrollBar 繼承於 ListBox
- 於 ListBoxWithScrollBar 的外觀配置中,加入 Slider,並預計將它用來作為 ScrollBar
- 調整上述 Slider 的外觀配置,讓它長得像 ScrollBar 該有的樣子
- 讓 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 沒有跟著移動