Introduction
This article explains how to create a ListBox
in WPF with the scrollbar on the background and auto-scrolling functionality when hovering over the control ends.
Background
A while ago, while working on a manufacturing application project, I created a view that displayed lists of manufacturing equipment on different rows, each row representing an equipment category. I started using a standard ListBox
control per row but I encountered two issues with the default appearance of the WPF LixtBox
. One was that the scrollbars were taking too much space on the view and they didn’t look good, and the other was that since there were so many listbox
es on the view, it was difficult to hit the small scrollbar buttons to move the equipment on each row.
The effect I was looking for was what you usually find on list controls on web pages when you hover over any of the list ends with the mouse and the list starts scrolling, but since the lists sometimes contained many elements, I also wanted to show the relative size of the list to the size of the ListBox
control without taking the extra space of the scrollbars.
The solution I ended up implementing was to put the scrollbar thumb on the background and the scroll buttons overlaying each end and only appearing when the mouse is over the ListBox
, so only one ListBox
on the view would display the buttons at once.
Step One: Use the Default Control Template as a Starting Point
If we want to modify the appearance of a standard WPF control, we have to replace the whole visual tree, so it is better to start using the default template XAML and modify the parts we want.
You can get the default control templates from this link.
This is the ListBox
template example from that page:
<Style x:Key="{x:Type ListBox}" TargetType="ListBox">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="MinHeight" Value="95"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border Name="Border" Background="{StaticResource WindowBackgroundBrush}"
BorderBrush="{StaticResource SolidBorderBrush}"
BorderThickness="1" CornerRadius="2">
<ScrollViewer Margin="0" Focusable="false">
<StackPanel Margin="2" IsItemsHost="True" />
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource DisabledBackgroundBrush}" />
<Setter TargetName="Border" Property="BorderBrush"
Value="{StaticResource DisabledBorderBrush}" />
</Trigger>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Step Two: Use PART_XXX Names to Replace the Appearance of the Scrollbar Elements but Maintaining the Built-in Logic.
Some standard WPF controls have built-in logic that is tied to specific elements on the visual tree based on the element’s name. All these elements follow the name pattern “PART_XXX
”.
When you replace the control’s visual tree by assigning a new ControlTemplate
to the Template
property, the control looks for elements on the new template with those specific names (sometimes with specific types too) and if they are found, the built-in logic is applied to them.
In our case, we need to use three of these names: PART_HorizontalScrollBar
and PART_VerticalScrollBar
on each of the control templates for horizontal and vertical ListBoxstyles
, and PART_Track
on the template for the scrollbar itself.
Step Three: Move the Scrollbar to the Background
To display the ListBox
items, we use a ScrollViewer
and replace the default ControlTemplate
with a Grid
control. This allows us to overlay controls on top of each other and each one taking the whole space available on the grid. We put the scrollbar on the background and on top of it a ScrollContentPresenter
with a small vertical margin for the horizontal ListBox
version and a horizontal margin for the vertical version so we can see the scrollbar behind the ListBox
elements and also drag it with the mouse.
We then give the scrollbar the mentioned “PART_XXX
” name and replace its control template because we don’t want to display the scrollbar buttons on this layer.
The only requirement for the scrollbar template is that it must contain a Track
element with name “PART_Track
”. Track is not a control (it derives directly from FrameworkElement
) but it exposes its three elements through properties (Thumb
, DecreaseRepeatButton
, and IncreaseRepeatButton
) so we can change its appearance through them. Both buttons are transparent by default and that’s what gives the effect of moving a page when you click on each side of the scrollbar thumb.
This is the resultant code:
<ScrollViewer x:Name="scrollviewer" >
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}" >
<Grid>
<ScrollBar x:Name="PART_HorizontalScrollBar" Orientation="Horizontal"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Height="{Binding Height, ElementName=Panel}">
<ScrollBar.Template>
<ControlTemplate>
<Track x:Name="PART_Track">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand"
Style="{StaticResource ScrollBarPageButton}"/>
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand"
Style="{StaticResource ScrollBarPageButton}"/>
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumb}"
Background="Gray" Opacity="0.8" Margin="0,-1" />
</Track.Thumb>
</Track>
</ControlTemplate>
</ScrollBar.Template>
</ScrollBar>
<ScrollContentPresenter Margin="0,2" Height="Auto"
VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</ScrollViewer.Template>
<ItemsPresenter/>
</ScrollViewer>
Step Four: Show the Scrollbar Buttons on a Layer on Top of the ListBox Content
To do this, we add a transparent Grid
on top of the ScrollViewer
showed on the previous step. We divide the grid into three columns and put the buttons on the exterior columns. To get the effect we want, we use RepeatButtons
and set the ClickMode
property to “Hover
”. That keeps firing click events as long as the mouse is over the button.
The buttons should only appear when the mouse is over the ListBox and the number of elements on the list exceeds the available space on the control. To accomplish that, we need to set the opacity to 0
and use an animation on the next step to fade in the button to make it visible. If we only do that, the buttons are going to interfere with the mouse interactions with the ListBox
items even if they are not visible, so we also set the visibility to “Collapsed
” and later we’ll set it to “Visible
” at the same time we start the fade-in animation.
<Grid x:Name="Panel" Margin="0,2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<RepeatButton x:Name="LineLeftButton" Grid.Column="0"
Width="20" Opacity="0" Visibility="Collapsed"
Style="{StaticResource ScrollBarLineButton}"
Content="M 8 0 L 8 32 L 0 16 Z"
Command="{x:Static ScrollBar.LineLeftCommand}"
CommandTarget="{Binding ElementName=scrollviewer}"
ClickMode="Hover" />
<RepeatButton x:Name="LineRightButton" Grid.Column="2"
Width="20" Opacity="0" Visibility="Collapsed"
Style="{StaticResource ScrollBarLineButton}"
Content="M 0 0 L 8 16 L 0 32 Z"
Command="{x:Static ScrollBar.LineRightCommand}"
CommandTarget="{Binding ElementName=scrollviewer}"
ClickMode="Hover"/>
</Grid>
Step Five: Animate the Buttons into View When Mouse Hovers Over
We use a MultiTrigger
with two conditions: IsMouseOver
doesn’t require any explanation and ComputedHorizontalScrollBarVisibility
is a property on the ScrollViewer
control that indicates if the ScrollBar
should be visible. We already set the HorizontalScrollBarVisibility
to “Auto
” so the scrollbar should only be visible when the number of elements exceeds the available space on the control and this is the effect we are looking for.
We add a couple of setters to make the buttons visible and the Opacity animations to fade-in and fade-out the buttons. Finally, only when both conditions are true
, the setters and actions are executed, and that completes all the requirements we had for this control.
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="ComputedHorizontalScrollBarVisibility"
SourceName="scrollviewer" Value="Visible"/>
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter TargetName="LineLeftButton" Property="Visibility" Value="Visible" />
<Setter TargetName="LineRightButton" Property="Visibility" Value="Visible" />
</MultiTrigger.Setters>
<MultiTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="LineLeftButton"
Storyboard.TargetProperty="Opacity" To="0.8" Duration="0:0:0.25"/>
<DoubleAnimation Storyboard.TargetName="LineRightButton"
Storyboard.TargetProperty="Opacity" To="0.8" Duration="0:0:0.25"/>
</Storyboard>
</BeginStoryboard>
</MultiTrigger.EnterActions>
<MultiTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="LineLeftButton"
Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.25"/>
<DoubleAnimation Storyboard.TargetName="LineRightButton"
Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.25"/>
</Storyboard>
</BeginStoryboard>
</MultiTrigger.ExitActions>
</MultiTrigger>
</ControlTemplate.Triggers>
Step Six: Remove the Blue Background on the ListBox Selected Item
To remove that blue background, we have to replace the ItemContainerStyle
on the ListBox
. We can do that with a setter on the Style
, so it is applied by default to the ListBox
es that use our Style
.
This is the ItemContainerStyle
I used:
<Style x:Key="CustomListBoxItem" TargetType="{x:Type ListBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="Border" Padding="1" SnapsToDevicePixels="true">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
History
- 08/Mar/2010: Initial version