Styling the WPF ScrollViewer

Use WPF ScrollViewer to create scrollbars resemble those in Yoono, a Google Chrome extension. To do this, I use a MultiTrigger and a custom ControlTemplate in XAML.

Introduction

I saw a Google Chrome extension called Yoono. Basically, it has a list box containing tweets or status updates. One thing I noticed is how the list box’s scrollbar is styled.

Figure 1. Scrollbar Style in Yoono

Figure 1 shows cropped images of the list box taken at different times. When the mouse is outside the bounds of the list box, the scrollbar is not shown. If the user moves the mouse inside the list box, a small scrollbar will appear. When the user moves the mouse over this scrollbar, the scrollbar will become larger. I thought of applying this behavior to a WPF ScrollViewer.

Get the Default ScrollViewer Style

The first thing we usually do when restyling a control is to get the default style of the control and modify it instead of starting from scratch. The default style of the ScrollViewer is shown below.

<Style x:Key="{x:Type ScrollViewer}"
    TargetType="{x:Type ScrollViewer}">
    <Style.Triggers>
        <Trigger Property="IsEnabled"
                Value="false">
            <Setter Property="Foreground"
                Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
        </Trigger>
    </Style.Triggers>
</Style>

Listing 1. ScrollViewer Default Style

You might have thought that the ScrollViewer’s default template contains two ScrollBar controls, vertical and horizontal. Actually, the ScrollBar controls are added via code, in the ScrollViewer’s static constructor. If we translate the code to XAML, the default template would look something like this.

<ControlTemplate TargetType="{x:Type ScrollViewer}">
    <Grid Background="{TemplateBinding Background}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ScrollBar
            x:Name="PART_VerticalScrollBar"
            Grid.Column="1"
            Minimum="0.0"
            Maximum="{TemplateBinding ScrollableHeight}"
            ViewportSize="{TemplateBinding ViewportHeight}"
            Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=VerticalOffset, Mode=OneWay}"
            Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"        
            Cursor="Arrow"
            AutomationProperties.AutomationId="VerticalScrollBar"/>
        <ScrollBar
            x:Name="PART_HorizontalScrollBar"
            Orientation="Horizontal"
            Grid.Row="1"
            Minimum="0.0"
            Maximum="{TemplateBinding ScrollableWidth}"
            ViewportSize="{TemplateBinding ViewportWidth}"
            Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=HorizontalOffset, Mode=OneWay}"
            Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
            Cursor="Arrow"
            AutomationProperties.AutomationId="HorizontalScrollBar"/>
        <ScrollContentPresenter
            x:Name="PART_ScrollContentPresenter"
            Margin="{TemplateBinding Padding}"
            Content="{TemplateBinding Content}"
            ContentTemplate="{TemplateBinding ContentTemplate}"
            CanContentScroll="{TemplateBinding CanContentScroll}"/>
        <Rectangle
            x:Name="Corner"
            Grid.Column="1"
            Grid.Row="1"
            Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
    </Grid>
</ControlTemplate>

Listing 2. Default ScrollViewer Template in XAML

Other controls, like the DataGrid, override the default template of the ScrollViewer. It means that in case we want to modify the ScrollViewer in the DataGrid, we have to change the DataGrid style also. Modifying the default template of the ScrollViewer does not mean that it will get applied to ScrollViewers used by other controls. For this article, we will just delve on modifying the default template.

Hide and Show ScrollBars

For demonstration purposes, I created a WPF application with a ScrollViewer containing an Image.

Figure 2. Demo WPF Application

In the default ScrollViewer template, the Grid has four cells for the following: ScrollViewer content, vertical scrollbar, horizontal scrollbar, and a rectangle. Since we want the ScrollBars to be semi-transparent over the ScrollViewer content, we can put the ScrollBars on a separate Grid that will overlap with the other Grid. The Rectangle in the default template can also be removed. The Rectangle just fills the lower-right corner with gray color. The following listing shows the modified ScrollViewer style.

<Style x:Key="{x:Type ScrollViewer}"
    TargetType="{x:Type ScrollViewer}">
    <Setter Property="HorizontalScrollBarVisibility" Value="Hidden"/>
    <Setter Property="VerticalScrollBarVisibility" Value="Hidden"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ScrollViewer}">
                <Grid>
                    <Grid Background="{TemplateBinding Background}">
                        <ScrollContentPresenter
                            x:Name="PART_ScrollContentPresenter"
                            Margin="{TemplateBinding Padding}"
                            Content="{TemplateBinding Content}"
                            ContentTemplate="{TemplateBinding ContentTemplate}"
                            CanContentScroll="{TemplateBinding CanContentScroll}"/>
                    </Grid>
                    <Grid Background="Transparent">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>                                
                        <ScrollBar
                            x:Name="PART_VerticalScrollBar"
                            Grid.Column="1"
                            Minimum="0.0"
                            Maximum="{TemplateBinding ScrollableHeight}"
                            ViewportSize="{TemplateBinding ViewportHeight}"
                            Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=VerticalOffset, Mode=OneWay}"
                            Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"        
                            Cursor="Arrow"
                            AutomationProperties.AutomationId="VerticalScrollBar"/>
                        <ScrollBar
                            x:Name="PART_HorizontalScrollBar"
                            Orientation="Horizontal"
                            Grid.Row="1"
                            Minimum="0.0"
                            Maximum="{TemplateBinding ScrollableWidth}"
                            ViewportSize="{TemplateBinding ViewportWidth}"
                            Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=HorizontalOffset, Mode=OneWay}"
                            Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
                            Cursor="Arrow"
                            AutomationProperties.AutomationId="HorizontalScrollBar"/>                                
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="IsEnabled"
                Value="false">
            <Setter Property="Foreground"
                Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
        </Trigger>
        <Trigger Property="IsMouseOver"
                Value="true">
            <Setter Property="HorizontalScrollBarVisibility"
                Value="Visible"/>
            <Setter Property="VerticalScrollBarVisibility"
                Value="Visible"/>
        </Trigger>
    </Style.Triggers>
</Style>

Listing 3. Modified ScrollViewer Style

In the modified style, the HorizontalScrollBarVisibility and VerticalScrollBarVisibility properties are set to Hidden. A trigger is used so that when the IsMouseOver property is set to True, the ScrollBars will be visible. The Background of the upper-layer Grid is set to Transparent so that the ScrollViewer content is not blocked from view.

Figure 3. Showing ScrollBars When Mouse is Over the ScrollViewer

The first window in Figure 3 has no ScrollBars since the mouse is outside the window. In the second window, the ScrollBars are visible because the mouse is inside the ScrollViewer. The ScrollBars still look the same as we still have not changed the ScrollBar template yet. Let’s try making the small version of the ScrollBar.

Small ScrollBars

Same thing with the ScrollViewer, we can get the default style of the ScrollBar control and modify the template to make the ScrollBar look smaller. I would like to show the default style here to show the differences with the modified style but it is quite long. If you like to see the default ScrollBar style, you can use a tool to get it or download the WPF Themes on this site: http://code.msdn.microsoft.com/wpfsamples#. The following listing shows a modified vertical ScrollBar template. The horizontal ScrollBar template is very similar so there is no need to show it here.

<ControlTemplate TargetType="{x:Type ScrollBar}">
    <Border
        x:Name="Bg"
        CornerRadius="2"                        
        Margin="2"
        Opacity="0.75"
        Background="{TemplateBinding Background}"
        VerticalAlignment="Bottom">
        <Grid
            SnapsToDevicePixels="true">
            <Grid.RowDefinitions>                                    
                <RowDefinition MaxHeight="{DynamicResource {x:Static SystemParameters.VerticalScrollBarButtonHeightKey}}"/>
                <RowDefinition MaxHeight="{DynamicResource {x:Static SystemParameters.VerticalScrollBarButtonHeightKey}}"/>
            </Grid.RowDefinitions>
            <RepeatButton
                Style="{StaticResource ScrollBarButton}"
                Background="#FFCBCBCB"                                    
                IsEnabled="{TemplateBinding IsMouseOver}"
                Command="{x:Static ScrollBar.LineUpCommand}"
                theme:ScrollChrome.ScrollGlyph="UpArrow"
                Margin="0,4"/>
            <RepeatButton                  
                Style="{StaticResource ScrollBarButton}"
                Background="#FFCBCBCB"
                Grid.Row="1"
                IsEnabled="{TemplateBinding IsMouseOver}"
                Command="{x:Static ScrollBar.LineDownCommand}"
                theme:ScrollChrome.ScrollGlyph="DownArrow"
                Margin="0,4">
            </RepeatButton>
        </Grid>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled"
                Value="false">
            <Setter TargetName="Bg"
                Property="Visibility"
                Value="Hidden"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Listing 4. Modified ScrollBar Style

In the default template, the Track which contains the ScrollBar Thumb control is sandwiched between two RepeatButtons. In the modified template, the Track is removed and now only contains the two RepeatButtons. The Track will be shown when the mouse is over the ScrollBar. On the other hand, we still haven’t changed the template of the RepeatButtons. The default RepeatButton template is shown below.

<ControlTemplate TargetType="{x:Type RepeatButton}">
    <theme:ScrollChrome Name="Chrome"
                    ScrollGlyph="{TemplateBinding theme:ScrollChrome.ScrollGlyph}"
                    RenderMouseOver="{TemplateBinding IsMouseOver}"
                    RenderPressed="{TemplateBinding IsPressed}"
                    SnapsToDevicePixels="true"/>
</ControlTemplate>

Listing 5. ScrollBarButton Style

Since a ScrollChrome is used as the template, nothing much could be done but to remove it and supply a different template. We only need to specify an arrow shape in the template.

<ControlTemplate TargetType="{x:Type RepeatButton}">
    <Path x:Name="Arrow" HorizontalAlignment="Center" VerticalAlignment="Center" Fill="{TemplateBinding Background}"/>
    <ControlTemplate.Triggers>
        <Trigger Property="theme:ScrollChrome.ScrollGlyph" Value="UpArrow">
            <Setter TargetName="Arrow" Property="Data" Value="M 3,0 l 3,8 l -6,0 Z"/>
        </Trigger>
        <Trigger Property="theme:ScrollChrome.ScrollGlyph" Value="DownArrow">
            <Setter TargetName="Arrow" Property="Data" Value="M 0,0 l 6,0 l -3,8 Z"/>
        </Trigger>
        <Trigger Property="theme:ScrollChrome.ScrollGlyph" Value="LeftArrow">
            <Setter TargetName="Arrow" Property="Data" Value="M 0,3 l 8,-3 l 0,6 Z"/>
        </Trigger>
        <Trigger Property="theme:ScrollChrome.ScrollGlyph" Value="RightArrow">
            <Setter TargetName="Arrow" Property="Data" Value="M 0,0 l 8,3 l -8,3 Z"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Listing 6. Arrow Template

In here, the Path shape is used to create the arrows. Triggers determine the direction of the arrow and set the Path data accordingly. The ScrollGlyph property is reused so that we don’t have to define our own enumeration for the arrow directions. It also means that we don’t have to modify the RepeatButtons that use the ScrollGlyph property. Meanwhile, in the ScrollBar template, a Border is added to serve as the background for the arrows. Some margins were also added.

Figure 4. Small ScrollBar

Lastly, we need to show the larger version of the ScrollBar when the mouse is over it.

Large ScrollBars

Remember in Listing 4 that we specified a small ScrollBar as the template. When the IsMouseOver property of the ScrollBar is set to true, we can set the template to the large version of the ScrollBar. The following code listing shows the trigger that accomplishes this for the vertical ScrollBar.

<MultiTrigger>
    <MultiTrigger.Conditions>
        <Condition Property="IsMouseOver" Value="True"/>
        <Condition Property="Orientation" Value="Vertical"/>
    </MultiTrigger.Conditions>
    <MultiTrigger.Setters>
        <Setter Property="Width"
            Value="30"/>
        <Setter
            Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ScrollBar}">
                    <Border
                        CornerRadius="4"                        
                        Margin="2"
                        Opacity="0.75"
                        Background="{TemplateBinding Background}">
                        <Grid
                            SnapsToDevicePixels="true">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="0.00001*"/>
                                <RowDefinition MaxHeight="{DynamicResource {x:Static SystemParameters.VerticalScrollBarButtonHeightKey}}"/>
                                <RowDefinition MaxHeight="{DynamicResource {x:Static SystemParameters.VerticalScrollBarButtonHeightKey}}"/>
                            </Grid.RowDefinitions>
                            <Track
                                Name="PART_Track"
                                IsEnabled="{TemplateBinding IsMouseOver}"
                                IsDirectionReversed="true">
                                <Track.DecreaseRepeatButton>
                                    <RepeatButton Style="{StaticResource VerticalScrollBarPageButton}"
                                        Command="{x:Static ScrollBar.PageUpCommand}"/>
                                </Track.DecreaseRepeatButton>
                                <Track.IncreaseRepeatButton>
                                    <RepeatButton Style="{StaticResource VerticalScrollBarPageButton}"
                                        Command="{x:Static ScrollBar.PageDownCommand}"/>
                                </Track.IncreaseRepeatButton>
                                <Track.Thumb>
                                    <Thumb Style="{StaticResource ScrollBarThumb}"
                                        theme:ScrollChrome.ScrollGlyph="VerticalGripper"
                                        Margin="2"/>
                                </Track.Thumb>
                            </Track>
                            <RepeatButton
                                Style="{StaticResource ScrollBarButton}"
                                Background="#FFFFFFFF"
                                Grid.Row="1"
                                IsEnabled="{TemplateBinding IsMouseOver}"
                                Command="{x:Static ScrollBar.LineUpCommand}"
                                theme:ScrollChrome.ScrollGlyph="UpArrow"
                                RenderTransformOrigin="0.5, 0.5">
                                <RepeatButton.RenderTransform>
                                    <ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
                                </RepeatButton.RenderTransform>
                            </RepeatButton>
                            <RepeatButton                          
                                Style="{StaticResource ScrollBarButton}"
                                Background="#FFFFFFFF"
                                Grid.Row="2"
                                IsEnabled="{TemplateBinding IsMouseOver}"
                                Command="{x:Static ScrollBar.LineDownCommand}"
                                theme:ScrollChrome.ScrollGlyph="DownArrow"
                                RenderTransformOrigin="0.5, 0.5">
                                <RepeatButton.RenderTransform>
                                    <ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
                                </RepeatButton.RenderTransform>
                            </RepeatButton>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </MultiTrigger.Setters>
</MultiTrigger>

Listing 7. Large ScrollBar Template

A MultiTrigger is used because we have to take into consideration the IsMouseOver and Orientation properties. The most noticeable difference of this template compared to that in Listing 4 is the addition of the Track. A ScaleTransform is also applied to both RepeatButtons so that the arrows look larger. The ScrollBar Thumb control default template uses a ScrollChrome also. I changed this to a Border and just set the appropriate Background and CornerRadius values to make the ScrollBar look similar to that in Figure 1.

Figure 5. Large ScrollBar

That’s it. We now have a different-styled ScrollViewer. Using WPF, we easily copied the style that we want and do it using only XAML. You can download the Visual Studio 2010 solution here.

By Michael Detras   Popularity  (18095 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog