Introduction
Some applications display tool icons on the left or right side. But if the user decides to make his window smaller, some of the icons are cut off. We would like to use a scroll bar, but it would look silly to have a standard scrollbar for a bunch of icons. Instead, we would like to have scroll arrows at the top and bottom of the icons.
The article presents a technique which can be used to produce a vertical scrolling area with buttons at the top and bottom but no bar. Or, you could produce a horizontal scrolling area with buttons at the left and right but no bar. The application contains both a vertical scrolling area and a horizontal scrolling area.
Prerequisites
The sample application is written in Visual Studio 2010 with WPF 4.
Understanding the code
For the example application, I used small colored regions with shapes or text in them, instead of actual icons. That is just because I'm not an artist and I didn't want the sample application to have a bunch of attached image files.
I put my colored rectangles in a StackPanel
because StackPanel
implements IScrollInfo
, which means that you can set the ScrollViewer
to scroll one item at a time. Item-by-item scrolling is also called "logical scrolling". If you were using a different container, such as a Grid
or a DockPanel
, the scrollbar would not scroll cleanly between items.
In your ScrollViewer
, you need to set CanContentScroll="True"
to get the logical scrolling to work. If you want the standard physical scrolling, set CanContentScroll="False"
instead.
<ScrollViewer x:Name="VerticalScroller"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Disabled"
CanContentScroll="True"
SizeChanged="VerticalScrollViewer_SizeChanged"
Loaded="VerticalScrollViewer_Loaded"
ScrollChanged="VerticalScrollViewer_ScrollChanged">
<StackPanel x:Name="VerticalContentPanel">
...
With this approach, we are using our own arrow buttons, so set VerticalScrollBarVisibility="Hidden"
. This setting means that the StackPanel
will scroll vertically, but WPF won't draw any scrolling controls (like the scroll bars or buttons) for you.
The buttons which control scrolling are RepeatButton
s instead of ordinary Button
s. When I used Blend to look at the control template for a ScrollViewer
and its child ScrollBar
, I saw that each button in the ScrollBar
(the Up button and the Down button) were of type RepeatButton
, which has special behavior: if you hold it down, it repeats the action. So I decided to use the same type of button for my up and down buttons. I also copied the styling for my RepeatButton
from the control template.
When the scroll viewer is loaded, I save the scrollbar and the Up and Down buttons in member variables. This is accomplished by getting the object's ControlTemplate
and then calling FindName()
.
private ScrollBar _verticalScrollBar;
private RepeatButton _upButton;
private RepeatButton _downButton;
private void VerticalScrollViewer_Loaded(object sender,
System.Windows.RoutedEventArgs e)
{
ScrollViewer scrollViewer = sender as ScrollViewer;
_verticalScrollBar = scrollViewer.Template.FindName(
"PART_VerticalScrollBar", scrollViewer) as ScrollBar;
_upButton = _verticalScrollBar.Template.FindName("PART_UpButton",
_verticalScrollBar) as RepeatButton;
_downButton = _verticalScrollBar.Template.FindName("PART_DownButton",
_verticalScrollBar) as RepeatButton;
UpdateVerticalScrollBarButtons();
}
When the scrollbar is loaded, I also call UpdateVerticalScrollBarButtons()
which calculates whether the scroll buttons should be visible. This method is also called in response to the SizeChanged
and ScrollChanged
events. Either of these events may cause the scroll button visibility to change. For example, here is the ScrollChanged
handler:
private void VerticalScrollViewer_ScrollChanged(object sender,
System.Windows.Controls.ScrollChangedEventArgs e)
{
UpdateVerticalScrollBarButtons();
}
The core of this project is the code which sets the scroll button visibility. The key idea is: if there is enough space to display each icon (or any other type of object; in the Demo project they are Border
objects) within the StackPanel
, then I want to hide both scroll buttons.
To calculate how much space all the StackPanel
children would take up, I simply iterate through them and get the height of each one.
double desiredPanelHeight = 0;
foreach (UIElement uiElement in VerticalContentPanel.Children)
{
if (uiElement is FrameworkElement)
{
FrameworkElement wpfElement = (FrameworkElement)uiElement;
desiredPanelHeight += wpfElement.Height;
}
}
To figure out whether there is enough space available, I get the height of the ScrollBar
. Since the ScrollViewer
is in a grid row whose height is *, the ScrollBar
will be the largest it possibly can be. In addition, if either of the scroll buttons is currently visible, I add in its scroll bar button height, because these scroll buttons will be collapsed if there is enough space for the StackPanel
content. So here is the calculation of how much room is available:
double availablePanelHeight = VerticalScroller.ActualHeight;
if (UpButton.Visibility == Visibility.Visible)
availablePanelHeight += UpButton.Height;
if (DownButton.Visibility == Visibility.Visible)
availablePanelHeight += DownButton.Height;
By comparing the two calculated heights (desiredPanelHeight
and availablePanelHeight
), we can tell whether scroll buttons will be needed, but we still want to hide the Up button if the scroll position is at the top, or hide the Down button if the scroll position is at the bottom. These calculations take up the rest of the method:
Visibility upButtonVisibility;
Visibility downButtonVisibility;
if (availablePanelHeight < desiredPanelHeight)
{
bool isAtTheTop = false;
bool isAtTheBottom = false;
if (_verticalScrollBar != null)
{
if (_verticalScrollBar.Value == _verticalScrollBar.Maximum)
isAtTheBottom = true;
if (_verticalScrollBar.Value == _verticalScrollBar.Minimum)
isAtTheTop = true;
}
if (isAtTheTop)
upButtonVisibility = Visibility.Collapsed;
else
upButtonVisibility = Visibility.Visible;
if (isAtTheBottom)
downButtonVisibility = Visibility.Collapsed;
else
downButtonVisibility = Visibility.Visible;
}
else
{
upButtonVisibility = Visibility.Collapsed;
downButtonVisibility = Visibility.Collapsed;
}
UpButton.Visibility = upButtonVisibility;
DownButton.Visibility = downButtonVisibility;
The only thing left is handling the click events. When the Up button is pressed, we call ScrollBar.LineUp
, and when the Down button is pressed, we call ScrollBar.LineDown
.
private void UpButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
VerticalScroller.LineUp();
}
private void DownButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
VerticalScroller.LineDown();
}
Other ideas which didn't work out
Initially, I tried modifying the control template for the ScrollViewer
and its child ScrollBar
. Although I was able to move the arrows to the top and bottom and hide the bars, I could not figure out a way to consistently and reliably hide and show the scroll bar buttons. Also, I couldn't get the scroll bar buttons to enable and disable. It seems like a standard scroll bar doesn't really support these, and perhaps it's not necessary. For example, you can visually see that the scroll bar is at the top, so you don't really need to disable the top arrow.
Points of interest
Before I did this, I wondered why anyone would ever set VerticalScrollBarVisibility="Hidden"
. Now I know. The region is scrollable but you handle its movement yourself.
History
- June 26, 2011: Added a picture of the application.