Introduction
Many WPF controls like the TreeView, ListView, and so forth, use a scrollbar and it is sometimes useful to use the UI space that a scrollbar covers, not just for the bar itself, but also for a button or even a mini tool bar. This tip explains the "how to" bit using a ListBox, but the same principal should really be applicable to any other control that uses a ScrollViewer
since it is the ScrollViewer
that it customized in the end.
Background
I was looking for a solution to put a ResizeGrip into the otherwise blank button - right corner of a pop-up control, but I found only bits and pieces, nevermind a complete solution. So, I developed a solution and am going to document the sample solution here in the hope that it can be useful to others.
The screenshots below shows a ListBox in the attached sample application. Notice that the ResizeGrip is positioned where you normally have a blank area.
Using the code
To understand this solution it is important to understand the concept of a ControlTemplate in WPF. A ControlTemplate
is a very flexible way to completely re-define the order and way in which the parts of a control are layed out. To do this, we can simply download a sample ControlTemplate
modify it as we see fit, and insert it into XAML using the Template
dependency property.
To change the layout of the scrollbars of a ListBox, we are required to first re-template the ListBox, and then change the ScrollViewer's ControlTemplate
inside that ListBox. So, here is the ListBox with the Template
dependency property in the attached sample application:
<ListBox Template="{StaticResource defaulttemplate}"
ItemsSource="{Binding MyStrings,RelativeSource={RelativeSource AncestorType={x:Type Window},Mode=FindAncestor}}"
/>
We can see that the Template
dependency property points to a static ControlTemplate
resource called defaulttemplate:
<ControlTemplate x:Key="defaulttemplate" TargetType="{x:Type ListBox}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Padding="1"
SnapsToDevicePixels="True">
<ScrollViewer Focusable="False"
Padding="{TemplateBinding Padding}"
Template="{StaticResource ScrollViewTemplate}"
>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
The defaulttemplate of the ListBox in turn contains the scrollview we are about to customize. To customize it, it contains a Template
reverance to another resource called ScrollViewTemplate:
<ControlTemplate TargetType="{x:Type ScrollViewer}" x:Key="ScrollViewTemplate">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Grid Background="{TemplateBinding Background}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollContentPresenter Grid.Column="0" Grid.ColumnSpan="2"
Grid.Row="0" Grid.RowSpan="2"
KeyboardNavigation.DirectionalNavigation="Local"
CanContentScroll="True"
CanHorizontallyScroll="True"
CanVerticallyScroll="True"
/>
<ScrollBar Name="PART_VerticalScrollBar"
Grid.Column="1" Grid.Row="0"
Padding="0,0,0,3"
Value="{TemplateBinding VerticalOffset}"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
<ScrollBar Name="PART_HorizontalScrollBar"
Grid.Column="0" Grid.Row="1"
Orientation="Horizontal"
Padding="0,0,6,0"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
<DockPanel Grid.Column="1" Grid.Row="1"
LastChildFill="false"
Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
<ResizeGrip HorizontalAlignment="Right" VerticalAlignment="Stretch"
Grid.Column="1" Grid.Row="1"
Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
/>
</Grid>
</Border>
</ControlTemplate>
The ControlTemplate
of the ScrollViewer is a little bit more complicated but is essantially a layout using a Grid
with 2 rows and 2 columns. The bottom row contains the horizontal scrollbar and the ResizeGrip
in the far right corner. The right column contains the vertical scrollbar with the same ResizeGrip
in the bottom row.
All we need to apply this, is to have the two ControlTemplates
in the Resource section of the application, and have the Template
dependency property of the ListBox point at the ListBox template. The same technique can obviously be used to position a button or a number of buttons on the bottom left side of the listbox.
Thats it, all I wanted to share in this tip. Be sure to download the sample application if you still have questions. Let me know how this works for you and whether you have similar examples of customization.