3

I have a ListView control where I want to resize the last column in sync with the size of the Window. So if the Window's width increases by 100 units, I want the columns' width to also increase by 100.

Should I be using the Resize event on the Window and use a magic number to resize the column header manually, sort of like?:

columnHeader.Width = windowSize.X - 400;
Joan Venge
  • 315,713
  • 212
  • 479
  • 689

2 Answers2

7

Here is a solution that uses databinding and some converter magic to get the job done.

First, let's describe a simple ListView with some data and 3 columns.

<ListView>
    <ListView.View>
        <GridView>
            <GridViewColumn Width="140" Header="Date" />
            <GridViewColumn Width="140" Header="Day" 
                            DisplayMemberBinding="{Binding DayOfWeek}" />
            <GridViewColumn Width="140" Header="Year" 
                            DisplayMemberBinding="{Binding Year}"/>
        </GridView>
    </ListView.View>

    <sys:DateTime>1/2/3</sys:DateTime>
    <sys:DateTime>4/5/6</sys:DateTime>
    <sys:DateTime>7/8/9</sys:DateTime>
</ListView>

This will get us to where you are. Now, in order to have the last column grow and shrink based on the parents width, we need to do build a converter and hook it up. First, let's adjust the last column of the GridView to make the width dynamic.

<GridViewColumn Header="Year" DisplayMemberBinding="{Binding Year}">
    <GridViewColumn.Width>
        <MultiBinding Converter="{StaticResource lastColumnMaximizerConverter}">
            <Binding Path="ActualWidth" 
                     RelativeSource="{RelativeSource AncestorType=ListView}"/>
            <Binding Path="View.Columns" 
                     RelativeSource="{RelativeSource AncestorType=ListView}"/>
        </MultiBinding>
    </GridViewColumn.Width>
</GridViewColumn>

What we've done here is created a MultiBinding object, hooked up an IMultiValueConverter, and described several parameters that we want to send into the IMultiValueConverter implementation. The first parameter is the ActualWidth of the parent ListView. The second parameter is the View.Columns collection on the parent ListView. We now have everything we need to calculate the final width of the last column in our view.

Now we need to create an IMultiValueConverter implementation. I happen to have one right here.

public class WidthCalculationMultiConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, 
                          object parameter, CultureInfo culture)
    {
        // do some sort of calculation
        double totalWindowWidth;
        double otherColumnsTotalWidth = 0;
        double.TryParse(values[0].ToString(), out totalWindowWidth);
        var arrayOfColumns = values[1] as IList<GridViewColumn>;

        for (int i = 0; i < arrayOfColumns.Count - 1; i++)
        {
            otherColumnsTotalWidth += arrayOfColumns[i].Width;
        }

        return (totalWindowWidth - otherColumnsTotalWidth) < 0 ? 
                     0 : (totalWindowWidth - otherColumnsTotalWidth);
    }

    public object[] ConvertBack(object value, Type[] targetTypes,
                                object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Psst.. by the way, this is not the safest code. You'll want to spruce it up! It's just a demo and you know how demo code is! :)

Last of all, we need to instantiate our Converter instance in our XAML.

<Grid.Resources>
    <Converters:WidthCalculationMultiConverter 
                  x:Key="lastColumnMaximizerConverter"/>
</Grid.Resources>

So now we have a a converter that will figure out how wide we want to make the last column, based on the widths of the columns (not including the last one) in the ListView and a binding that will use that converter and send in the required parameters and get the "right answer" out and apply it to the last column's width.

If you've put this together right, you should now have a ListView in which the last column will always stretch to the maximum width of the parent ListView.

I hope that this gets you going but also helped you understand how we can do this without writing some code-behind and using more of the facilities provided by WPF.

Dave White
  • 3,451
  • 1
  • 20
  • 25
  • Btw I forgot to ask but when setting the column width programmatically, will it be as if the user is resizing it by hand where the corresponding cells get resized too? – Joan Venge Apr 07 '11 at 00:02
  • I don't know if I'm understanding your question, but if you resize a column, all of the cells in that column grow to match the column width. You do not have to resize all of the cells as well. – Dave White Apr 07 '11 at 05:52
  • Thanks, this is exactly what I was asking. The reason I asked this is because when you hide a columnheader, it didn't hide the cells, so I assumed maybe scaling is also only applicable to the columnheader. – Joan Venge Apr 07 '11 at 16:37
2

I can't take full credit for this answer, because I got it here

But basically this is the idea:

Replace the "View" in the listView with a GridView. Then, you can specify whatever width you want for the first columns. In this case 100, 50, 50. Then we add a binding on the final column, that takes as input the parent ListView control. But we are allowing a converter to do the dirty work for us.

<ListView>
    <ListView.View>
      <GridView>
        <GridViewColumn Header="Title" DisplayMemberBinding="{Binding}" Width="100"/>
        <GridViewColumn Header="Title" DisplayMemberBinding="{Binding}" Width="50"/>
        <GridViewColumn Header="Title" DisplayMemberBinding="{Binding}" Width="50"/>
        <GridViewColumn Header="4" DisplayMemberBinding="{Binding}">
        <GridViewColumn.Width>
          <MultiBinding Converter="{StaticResource starWidthConverter}">
            <Binding Path="ActualWidth"  RelativeSource="{RelativeSource AncestorType=ListView}"/>
            <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}"/>
          </MultiBinding>
        </GridViewColumn.Width>
      </GridViewColumn>
     </GridView>
    </ListView.View>
    <ListViewItem>item1</ListViewItem>
    <ListViewItem>item2</ListViewItem>
    <ListViewItem>item3</ListViewItem>
    <ListViewItem>item4</ListViewItem>
  </ListView>

So, in the converter 'Convert' function we do this:

ListView listview = value[1] as ListView;
double width = listview.ActualWidth;
GridView gv = listview.View as GridView;
for(int i = 0;i < gv.Columns.Count-1;i++)
{
  if(!Double.IsNaN(gv.Columns[i].Width))
    width -= gv.Columns[i].Width;
}
return width - 5;// this is to take care of margin/padding

Which grabs the listview width, and calculates the new size.

Liz
  • 8,780
  • 2
  • 36
  • 40
  • Btw I forgot to ask but when setting the column width programmatically, will it be as if the user is resizing it by hand where the corresponding cells get resized too? – Joan Venge Apr 07 '11 at 00:03
  • Ok I added this but it doesn't do anything, the width seems like it's set to auto, but I added the converter and the binding, the view was already set to gridviewcolumn like in Dave's post. – Joan Venge Apr 07 '11 at 00:20
  • Sorry, I made a bad assumption that the code in the link I provided actually worked. Now it should work. – Liz Apr 07 '11 at 00:29
  • Oh, and to answer your second question... no not really. The corresponding cells don't really resize when the user resizes a column. They just move. Also, just a note, if the user touches the column width, or if later on you overwrite the column width in code, the binding is overwritten. See this link: http://stackoverflow.com/questions/181956/prevent-user-from-resizing-columns-with-wpf-listview to prevent this from happening. – Liz Apr 07 '11 at 00:35
  • Thanks Liz, I will give this a try and let you know for sure :O – Joan Venge Apr 07 '11 at 00:48
  • Based on your example, why are you using a MultiBinding? – Dave White Apr 07 '11 at 05:55
  • @Dave White - The first parameter is because I want to be notified when the ActualWidth of the listview changes. If I bind just to the list view, the converter will only be called at the beginning when the listview initializes. The second parameter I am passing the listview itself so I have access to the various parameters of the listview - i.e. the columns so I can measure how much space I need to use for the last column. – Liz Apr 07 '11 at 19:24
  • @Liz - Good point. My answer always watched the actual width, but when I saw your first answer with just the listView param, I thought "Oh.. simpler.. interesting..." but now it makes sense why you changed it. It wouldn't have worked with just the listview as a parameter. – Dave White Apr 07 '11 at 19:58
  • Btw guys I am still gonna return to this, this is just another tool, but I am on a new deadline for another tool which is for a couple of days, so will come back to this after that. – Joan Venge Apr 08 '11 at 00:57