Nedávno jsem potřeboval v Silverlight udělat možnost přidávání libovolného (0-n) počtu spolucestujících - objektů Osoba na detailu služební cesty. Normálně by se toto řešilo asi nějakým ListBoxem nebo rovnou DataGridem, kde budou jednotlivé přidávané osoby zobrazovány. V tomto případě, kde se sice přidává libovolný počet objektů, ale neočekává se jich obecně velké množství, by normální ListBox nebo DataGrid zabíral na formuláři detailu zbytečně moc místa. Rozhodl jsem se proto vytvořit UserControl komponentu, která bude prvky zobrazovat za sebou na jednom řádku, nebo když se nevejdou, tak control nabídne posouvání na další řádky (něco jako je např. zadávání emailových adres v řádku odesílatelů v Outlooku).
Pro řešení jsem zvolil použít ListBox control (změněný pomocí stylů a templates) a pro umístění jeho jednotlivých prvků jsem použil panel WrapPanel (ze Silverlight toolkitu). Podíváme se jak se něco takového dá napsat, a jaké problémy se zde řeší. Některé záležitosti co zde ukáži se samozřejmě dají využít i jinde, ne jen přímo v tomto controlu.
Vytvoření ListBox controlu a definice jeho ItemTemplate
Základní XAML controlu bude vypadat takto:
<UserControl x:Class="SilverlightRowListBox.OsobyRowListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit">
<ListBox x:Name="List" Height="26" ScrollViewer.HorizontalScrollBarVisibility="Disabled" KeyDown="List_KeyDown" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2,0,2,0">
<TextBlock Text="{Binding CeleJmeno}"/>
<TextBlock Margin="5,0,0,0" Text="(" Foreground="Silver"/>
<TextBlock Text="{Binding OsobniCislo}" />
<TextBlock Text=")" Foreground="Silver"/>
<Button Margin="3,0,0,0" IsTabStop="False" Click="btnRemove_Click">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Duration="0" To="#FF000000" Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="border"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed" />
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="border" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="border" Background="Transparent" BorderBrush="#00000000" BorderThickness="1" VerticalAlignment="Center" HorizontalAlignment="Center">
<Path Margin="1" Width="9" Height="9" Stretch="Fill" StrokeThickness="2" StrokeLineJoin="Round" Stroke="#FF000000" Fill="#FFFFFFFF" Data="M -3.5,3.5L 3.50001,-3.5M -3.50001,-3.5L 3.50001,3.5"/>
</Border>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
Změnu panelu pro prvky ListBoxu na WrapPanel provedeme změnou template ItemsPanel. Aby se ale WrapPanel choval tak, že pokud se prvky již nevejdou do jednoho řádku, přepadnou do řádku druhého, nastavíme panelu Orientation="Horizontal". Také musíme zakázat horizontální posuvník, to provedeme přímo na listboxu nastavením ScrollViewer.HorizontalScrollBarVisibility="Disabled".
Dále měníme prvek ListBoxu pomoci ItemTemplate, kromě zobrazení polí CeleJmeno a OsobniCislo jako součást prvku umístíme ještě symbol křížku, který bude sloužit na odebrání prvku ze seznamu. Toto řešíme tlačítkem, ze kterého strhneme standardní zobrazení a pomoci ControlTemplate vytvoříme zobrazení kompletně nové (křížek nadefinovaný pomoci Path). Dále přidáme stavy (VisualStateManager.VisualStateGroups) na zobrazení orámování (Border) při najetí myší (VisualState MouseOver) a skrytí celého tlačítka pokud není prvek přístupný (VisualState Disabled).
Ještě doplníme potřebný kód UserControlu, jedná se hlavně o obsluhu odstranění prvku v ListBoxu:
public partial class OsobyRowListBox : UserControl
{
#region constructors and destructors
public OsobyRowListBox()
{
InitializeComponent();
}
#endregion
#region property getters/setters
public ObservableCollection<Osoba> OsobyList
{
get { return (ObservableCollection<Osoba>)List.ItemsSource; }
set { List.ItemsSource = value; }
}
#endregion
#region private member functions
private void btnRemove_Click(object sender, RoutedEventArgs e)
{
var item = (Osoba)GetItemFromElement(List, sender as UIElement);
if (item == null)
{
return;
}
var list = (ObservableCollection<Osoba>)List.ItemsSource;
list.Remove(item);
}
private void List_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Delete || e.Key == Key.Back)
{
var item = (Osoba)GetItemFromElement(List, FocusManager.GetFocusedElement() as UIElement);
var list = (ObservableCollection<Osoba>)List.ItemsSource;
list.Remove(item);
List.Focus();
e.Handled = true;
}
}
private static object GetItemFromElement(ListBox listBox, UIElement element)
{
if (element != null)
{
//Get the object from the element
object data = DependencyProperty.UnsetValue;
while (data == DependencyProperty.UnsetValue)
{
//Try to get the object value for the corresponding element
data = listBox.ItemContainerGenerator.ItemFromContainer(element);
//Get the parent and we will iterate again
if (data == DependencyProperty.UnsetValue)
{
element = System.Windows.Media.VisualTreeHelper.GetParent(element) as UIElement;
}
//If we reach the actual listbox then we must break
if (element == listBox)
{
return null;
}
}
//Return the data that we fetched only if it is not Unset value
if (data != DependencyProperty.UnsetValue)
{
return data;
}
}
return null;
}
#endregion
}
V tomto kódu není asi nic moc co byste nečekali, stěžejní je pouze pomocná metoda GetItemFromElement, která vrací datový objekt pro ListBoxItem nebo jeho libovolného child element (v našem případě tlačítko s křížkem). (Pozn.: Někdy pro získání prvku vystačí vzít DataContext elementu, toto je ale obecnější způsob.)
Dosavadní výsledek controlu po jeho naplnění vypadá takto:
Control budeme dále ještě upravovat.
Enabled / Disabled prvků ListBoxItem
Jsou případy, kdy potřebujeme mít control pouze v režimu pro čtení, pokud bychom ale pouze nastavili IsEnabled=False pro celý ListBox, způsobilo by to jeho celé zablokování, a nemohli bychom například ovládat jeho posuvník. Proto tedy potřebujeme nastavit IsEnabled=False pouze všem prvkům ListBoxItem uvnitř ListBoxu.
Řešení, které zde ukáži, bude pomoci Binding přímo v XAMLu a to ve stylu prvků ListBoxItem. V UserControlu si nejprve připravíme vlastnost, přes kterou budeme moci enablování prvků ovládat, protože jí ale budeme používat přes Binding, nemůže to být vlastnost normální, ale musíme použít DependencyProperty. Nazveme jí IsItemsEnabled a její kód, který přidáme do našeho UserControlu bude tento:
public static readonly DependencyProperty IsItemsEnabledProperty = DependencyProperty.Register("IsItemsEnabled", typeof(bool), typeof(OsobyRowListBox), new PropertyMetadata(true));
[DefaultValue(true)]
public bool IsItemsEnabled
{
get { return (bool)base.GetValue(IsItemsEnabledProperty); }
set { base.SetValue(IsItemsEnabledProperty, value); }
}
Protože v Silverlight zatím nejde použít Binding výrazy RelativeSource FindAncestor (bude v Silverlight 5), použijeme k tomu abychom se dostali k vlastnosti IsItemsEnabled na UserControlu následující výraz:
{Binding ElementName=List, Path=Parent.IsItemsEnabled} (kde List je náš ListBox a jeho Parent je tedy náš UserControl).
V ListBoxu v jeho ItemContainerStyle uvedeme styl pro jeho prvky, v něm budeme pomoci Setter nastavovat vlastnost IsEnabled, potřebujeme tedy něco takového:
<Setter Property="IsEnabled" Value="{Binding ElementName=List, Path=Parent.IsItemsEnabled}"/>
To ovšem v Silverlight 4 také zatím možné není (je to možné ve WPF, a v Silverlight 5 to již bude také možné), tento problém se řeší dle tohoto článku pomocnou třídou SetterValueBindingHelper (prohlédnout si jí můžete přímo zde). Celý styl pro prvky listboxu bude tedy vypadat takto:
<ListBox ...
...
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem" >
<!--Original (WPF, Silverlight 5) code <Setter Property="IsEnabled" Value="{Binding ElementName=List, Path=Parent.IsItemsEnabled}"/>-->
<Setter Property="impcontrols:SetterValueBindingHelper.PropertyBinding">
<Setter.Value>
<impcontrols:SetterValueBindingHelper Property="IsEnabled" Binding="{Binding ElementName=List, Path=Parent.IsItemsEnabled}" />
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Změna posuvníku pro scrollování
Druhou úpravou, kterou na našem controlu provedeme, bude změna výchozího scrollbaru (který se nyní ani do našeho jednořádkového ListBoxu nevejde). Do Resources UserControlu přidáme následující styl pro ScrollViewer control. Styl vychází z výchozího stylu pro ScrollViewer (jak lze takový styl získat např. pomoci Blendu jsme si již v minulosti popisovali), je zde změněná grafika pro VerticalScrollBar (HorisontalScrollBar je úplně odstraněn):
<Style x:Key="ScrollViewerUpDownStyle" TargetType="ScrollViewer">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="VerticalScrollBarVisibility" Value="Visible"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9" Offset="0"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollContentPresenter x:Name="ScrollContentPresenter" Cursor="{TemplateBinding Cursor}" ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}"/>
<Rectangle Grid.Column="1" Fill="#FFE9EEF4" Grid.Row="1"/>
<ScrollBar x:Name="VerticalScrollBar" Grid.Column="1" IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" Margin="0,-1,-1,-1" Minimum="0" Orientation="Vertical" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{TemplateBinding VerticalOffset}" ViewportSize="{TemplateBinding ViewportHeight}" Width="36">
<ScrollBar.Style>
<Style TargetType="ScrollBar">
<Setter Property="MinWidth" Value="36"/>
<Setter Property="MinHeight" Value="17"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Grid x:Name="Root">
<Grid.Resources>
<ControlTemplate x:Key="VerticalIncrementTemplate" TargetType="RepeatButton">
<Grid x:Name="Root">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Background"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundGradient"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundMouseOver"/>
<ColorAnimation Duration="0" To="#7FFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#CCFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#F2FFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Background"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundGradient"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundPressed"/>
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="Highlight"/>
<ColorAnimation Duration="0" To="#6BFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#C6FFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#EAFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#F4FFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Duration="0:0:0" To=".7" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DisabledElement"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Rectangle x:Name="Background" Fill="#FF1F3B53" Opacity="0" RadiusY="2" RadiusX="2" StrokeThickness="1">
<Rectangle.Stroke>
<LinearGradientBrush EndPoint="1,.5" StartPoint="0,.5">
<GradientStop Color="#FF647480" Offset="1"/>
<GradientStop Color="#FFAEB7BF" Offset="0"/>
<GradientStop Color="#FF919EA7" Offset="0.35"/>
<GradientStop Color="#FF7A8A99" Offset="0.35"/>
</LinearGradientBrush>
</Rectangle.Stroke>
</Rectangle>
<Rectangle x:Name="BackgroundMouseOver" Fill="#FF448DCA" Opacity="0" RadiusY="2" RadiusX="2" Stroke="#00000000" StrokeThickness="1"/>
<Rectangle x:Name="BackgroundPressed" Fill="#FF448DCA" Opacity="0" RadiusY="2" RadiusX="2" Stroke="#00000000" StrokeThickness="1"/>
<Rectangle x:Name="BackgroundGradient" Margin="1" Opacity="0" RadiusY="1" RadiusX="1" Stroke="#FFFFFFFF" StrokeThickness="1">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="1,.7" StartPoint="0,.7">
<GradientStop Color="#FFFFFFFF" Offset="0.013"/>
<GradientStop Color="#F9FFFFFF" Offset="0.375"/>
<GradientStop Color="#E5FFFFFF" Offset="0.603"/>
<GradientStop Color="#C6FFFFFF" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle x:Name="Highlight" IsHitTestVisible="false" Margin="1" Opacity="0" RadiusY="1" RadiusX="1" Stroke="#FF6DBDD1" StrokeThickness="1"/>
<Path Data="F1 M 531.107,321.943L 541.537,321.943L 536.322,328.042L 531.107,321.943 Z " Height="4" Stretch="Uniform" Width="8">
<Path.Fill>
<SolidColorBrush x:Name="ButtonColor" Color="#FF333333"/>
</Path.Fill>
</Path>
<Rectangle x:Name="DisabledElement" Fill="#FFFFFFFF" Opacity="0" RadiusY="2" RadiusX="2"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="VerticalDecrementTemplate" TargetType="RepeatButton">
<Grid x:Name="Root">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Background"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundGradient"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundMouseOver"/>
<ColorAnimation Duration="0" To="#7FFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#CCFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#F2FFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Background"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundGradient"/>
<DoubleAnimation Duration="0:0:0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundPressed"/>
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="Highlight"/>
<ColorAnimation Duration="0" To="#6BFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#C6FFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#EAFFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
<ColorAnimation Duration="0" To="#F4FFFFFF" Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="BackgroundGradient"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Duration="0:0:0" To=".7" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DisabledElement"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Rectangle x:Name="Background" Fill="#FF1F3B53" Opacity="0" RadiusY="2" RadiusX="2" StrokeThickness="1">
<Rectangle.Stroke>
<LinearGradientBrush EndPoint="1,.5" StartPoint="0,.5">
<GradientStop Color="#FF647480" Offset="1"/>
<GradientStop Color="#FFAEB7BF" Offset="0"/>
<GradientStop Color="#FF919EA7" Offset="0.35"/>
<GradientStop Color="#FF7A8A99" Offset="0.35"/>
</LinearGradientBrush>
</Rectangle.Stroke>
</Rectangle>
<Rectangle x:Name="BackgroundMouseOver" Fill="#FF448DCA" Opacity="0" RadiusY="2" RadiusX="2" Stroke="#00000000" StrokeThickness="1"/>
<Rectangle x:Name="BackgroundPressed" Fill="#FF448DCA" Opacity="0" RadiusY="2" RadiusX="2" Stroke="#00000000" StrokeThickness="1"/>
<Rectangle x:Name="BackgroundGradient" Margin="1" Opacity="0" RadiusY="1" RadiusX="1" Stroke="#FFFFFFFF" StrokeThickness="1">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="1,.7" StartPoint="0,.7">
<GradientStop Color="#FFFFFFFF" Offset="0.013"/>
<GradientStop Color="#F9FFFFFF" Offset="0.375"/>
<GradientStop Color="#E5FFFFFF" Offset="0.603"/>
<GradientStop Color="#C6FFFFFF" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle x:Name="Highlight" IsHitTestVisible="false" Margin="1" Opacity="0" RadiusY="1" RadiusX="1" Stroke="#FF6DBDD1" StrokeThickness="1"/>
<Path Data="F1 M 541.537,173.589L 531.107,173.589L 536.322,167.49L 541.537,173.589 Z " Height="4" Stretch="Uniform" Width="8">
<Path.Fill>
<SolidColorBrush x:Name="ButtonColor" Color="#FF333333"/>
</Path.Fill>
</Path>
<Rectangle x:Name="DisabledElement" Fill="#FFFFFFFF" Opacity="0" RadiusY="2" RadiusX="2"/>
</Grid>
</ControlTemplate>
</Grid.Resources>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Duration="0" To="0.5" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="Root"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="VerticalRoot" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Rectangle RadiusY="1" RadiusX="1" Grid.ColumnSpan="2" Stroke="#00000000" StrokeThickness="1">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0,0.5" StartPoint="1,0.5">
<GradientStop Color="#FFF4F6F7" Offset="0"/>
<GradientStop Color="#FFF0F4F7" Offset="0.344"/>
<GradientStop Color="#FFDFE3E6" Offset="1"/>
<GradientStop Color="#FFE9EEF4" Offset="0.527"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Opacity=".375" RadiusY="1" RadiusX="1" Grid.ColumnSpan="2" StrokeThickness="1">
<Rectangle.Stroke>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9" Offset="0"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Stroke>
</Rectangle>
<Rectangle Margin="1" RadiusY="1" RadiusX="1" Grid.ColumnSpan="2">
<Rectangle.Stroke>
<LinearGradientBrush EndPoint="0.125,0.5" StartPoint="0.875,0.5">
<GradientStop Color="#33FFFFFF"/>
<GradientStop Color="#99FFFFFF" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Stroke>
</Rectangle>
<RepeatButton x:Name="VerticalSmallDecrease" Width="16" Height="18" IsTabStop="False" Interval="50" Margin="1" Grid.Column="0" Template="{StaticResource VerticalDecrementTemplate}"/>
<RepeatButton x:Name="VerticalSmallIncrease" Width="16" Height="18" IsTabStop="False" Interval="50" Margin="1" Grid.Column="1" Template="{StaticResource VerticalIncrementTemplate}"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ScrollBar.Style>
</ScrollBar>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
A ještě přidáme druhý styl pro ListBox, v něm je změněno pouze to, že vnořenému prvku ScrollViewer nastavíme, aby používal výše uvedený styl.
<Style x:Key="ListBoxStyle" TargetType="ListBox">
<Setter Property="Padding" Value="1"/>
<Setter Property="Background" Value="#FFFFFFFF"/>
<Setter Property="Foreground" Value="#FF000000"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="TabNavigation" Value="Once"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9" Offset="0"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Grid>
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2">
<ScrollViewer x:Name="ScrollViewer" BorderBrush="Transparent" BorderThickness="0" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" TabNavigation="{TemplateBinding TabNavigation}" Style="{StaticResource ScrollViewerUpDownStyle}">
<ItemsPresenter/>
</ScrollViewer>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Ještě musíme nastavit tento styl ListBoxu a tím máme změněnou grafiku posuvníku (viz příklad dole). Ještě je ale problém v tom, že posouvání není po celých řádkách tj. můžeme se dostat do stavu, že vidíme kus jednoho řádku a kus následujícího řádku. V tomto controlu, kde jsou všechny jeho prvky stejně vysoké (22px), by bylo mnohem lepší, pokud by se scrolování provádělo po celých řádcích. Tak to také standardní ListBox sám dělá, tuto funkcionalitu jsme “rozbili” tím, že jsme mu změnili panel na WrapPanel (normálně od SL3 používá ListBox jako základní panel control VirtualizingStackPanel, který posouvá po celých prvcích).
K tomu, abychom docílili jiného posouvání panelu uvnitř ListBoxu, musíme v panelu implementovat interface IScrollInfo (z System.Windows.Controls.Primitives namespace), ve kterém definujeme logiku posouvání. Nejedná se vůbec o jednoduchou záležitost, problém je totiž v tom, že je potřeba znovu implementovat metody MeasureOverride a ArrangeOverride, kde zabudujeme posun o nastavený VerticalOffset a HorizontalOffset. Já jsem takový kontrol napsal pro WrapPanel, řeším to tak, že provedu standardní Arrange a poté všechny prvky o offset posunu. (Z něho by se teoreticky dal analogicky copy/paste vytvořit kontrol i pro jiný druh panelu). Kontrol jsem pojmenoval ScrollableWrapPanel a jeho implementaci si můžete prohlédnout nebo stáhnout zde. Nyní tedy panel v listboxu zaměníme na tento:
<ItemsPanelTemplate>
<impcontrols:ScrollableWrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
Hotový příklad si zde můžete prohléhnout a vyzkoušet.