WPF wrap panel and scrolling

Flack picture Flack · Jan 22, 2010 · Viewed 57.2k times · Source

I have a simple WrapPanel which contains a number of wide controls. When I resize the Width of the Window everything works as expected. The controls will go across on a single line if there is enough space or wrap down to the next line when there isn't.

However, what I need to happen is that if all of the controls are basically stacked vertically (since there is no more horizontal space) and the Width of the Window is decreased even more, a horizontal scroll bar appears so that I can scroll and see the entire control if I want to. Below is my xaml. I tried wrapping the WrapPanel in a ScrollViewer but I couldn't achieve my goal.

<Window x:Class="WpfQuotes.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="Auto" Width="600" Foreground="White">
    <WrapPanel>
      <Button Width="250">1</Button>
      <Button Width="250">2</Button>
      <Button Width="250">3</Button>
    </WrapPanel>
</Window>

So if you reduce the Width of the above Window to its minimum, you will not be able to see the text of the buttons. I would like a horizontal scroll bar appear so that I can scroll to see the text but not interfere with the usual wrapping functionality.

Thanks.

Update: I have followed Paul's suggestion below and the horizontal scrollbar appears as expected now. However, I also wanted vertical scrolling available so I set VerticalScrollBarVisibility="Auto". The thing is, if I resize the window so that a vertical scroll bar appears, the horizontal one also always appears, even if it is not needed (there is enough horizontal space to see the entire control). It seems like the vertical scrollbar appearing is messing with the width of the scrollviewer. Is there a way to correct this so that the horizontal scrollbar doesn't appear unless it is actually needed?

Below is my xaml and the only code I added in the CustomWrapPanel:

<Window x:Class="Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cwp="clr-namespace:CustomWrapPanelExample"
        Title="Window1" Height="Auto" Width="300" Foreground="White" Name="mainPanel">
  <ScrollViewer x:Name="MyScrollViewer" HorizontalScrollBarVisibility="Auto"
                VerticalScrollBarVisibility="Auto">
    <cwp:CustomWrapPanel Width="{Binding ElementName=MyScrollViewer, Path=ActualWidth}">
      <Button Width="250">1</Button>
      <Button Width="250">2</Button>
      <Button Width="250">3</Button>
      <Button Width="250">4</Button>
      <Button Width="250">5</Button>
      <Button Width="250">6</Button>
      <Button Width="250">7</Button>
      <Button Width="250">8</Button>
      <Button Width="250">9</Button>
    </cwp:CustomWrapPanel>
  </ScrollViewer>
</Window>

The only thing overridden in CustomWrapPanel:

protected override Size MeasureOverride(Size availableSize)
{
  double maxChildWidth = 0;
  if (Children.Count > 0)
  {
    foreach (UIElement el in Children)
    {
      if (el.DesiredSize.Width > maxChildWidth)
      {
        maxChildWidth = el.DesiredSize.Width;
      }
    }
  }      
  MinWidth = maxChildWidth;
  return base.MeasureOverride(availableSize);
}

Answer

Paul Rohde picture Paul Rohde · Jan 22, 2010

Here's the thing, if your going to use a wrap panel, it does two things, it will take up as much available space in one direction, and expand as needed in the other. For instance, if you place it inside of a window like you have it, it takes up as much horizontal space as it can, and then expands as needed downward, that's why a vertical scroll bar will work, the parent container says "this is how wide I am, but you can make yourself as big as you want vertically", if you change it to a horizontal scroll bar, the scroll viewer is essentially saying "this is how tall you can be, but you can be as wide as you want" in this case the wrap panel doesn't wrap because there is no horizontal constraints.

One potential solution is to change the direction the wrap panel wraps from horizontal to vertical like this (Which is probably not the ideal or expected behavior):

    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
        <WrapPanel Orientation="Vertical">
            <Button Width="250">1</Button>
            <Button Width="250">2</Button>
            <Button Width="250">3</Button>
        </WrapPanel>
    </ScrollViewer>

In order to get the behavior your expecting, you'll have to do something closer to this:

    <ScrollViewer x:Name="MyScrollViewer" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
        <WrapPanel MinWidth="250" Width="{Binding ElementName=MyScrollViewer, Path=ViewportWidth}">
            <Button Width="250">1</Button>
            <Button Width="250">2</Button>
            <Button Width="250">3</Button>
        </WrapPanel>
    </ScrollViewer>

However, this second solution only works if you already know the width of your child elements, ideally you want your max width to be set to the actual width of the largest child item, but in order to do that you'd have to create a custom control that derives from wrap panel and write the code yourself to check for that.