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.

ApplicationPicture.PNG

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 1 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 stack panel 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 RepeatButtons instead of ordinary Buttons. 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 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 stack panel 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 2 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)
    {
        // scroll buttons are needed but we will still hide the Up button
        // if the scroll bar is at the top, and we will hide the Down button
        // if the scroll bar is at the bottom.

        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
    {
        // scroll bars are not needed
        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

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"