Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF Custom ListBox with Scrollbar on the Background

, 9 Mar 2010
Rate this:
Please Sign up or sign in to vote.
This article explains how to create a ListBox in WPF with the scrollbar in the background and auto-scrolling functionality when hovering over the control ends.
AutoScrollListBox

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 listboxes 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 ListBoxes 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Santiago Blanco-Leis
Software Developer (Senior) Pfizer/MSolutions
United States United States
No Biography provided

Comments and Discussions

 
QuestionButtons overlapped with Items Contents Pinmembersarcastizer28-May-13 23:10 
GeneralMy vote of 5 Pinmemberchjjo25-Sep-12 19:27 
BugPadding not working Pinmemberblackout_22-Nov-11 4:01 
GeneralRe: Padding not working Pinmemberblackout_24-Nov-11 5:00 
GeneralIs it possible to use Wrap panel as ItemsPanelTemplate PinmemberVinitYadav9-Apr-10 21:27 
GeneralRe: Is it possible to use Wrap panel as ItemsPanelTemplate PinmemberSantiago Blanco-Leis12-Apr-10 3:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140916.1 | Last Updated 9 Mar 2010
Article Copyright 2010 by Santiago Blanco-Leis
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid